pax_global_header00006660000000000000000000000064145460464420014523gustar00rootroot0000000000000052 comment=adff40b2928dbb2d22f27684e085f02d39a07291 markdown-exec-1.8.0/000077500000000000000000000000001454604644200142755ustar00rootroot00000000000000markdown-exec-1.8.0/.copier-answers.yml000066400000000000000000000013151454604644200200370ustar00rootroot00000000000000# Changes here will be overwritten by Copier _commit: 1.1.3 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli author_username: pawamoy copyright_date: '2022' copyright_holder: Timothée Mazzucotelli copyright_holder_email: pawamoy@pm.me copyright_license: ISC License insiders: true insiders_repository_name: markdown-exec project_description: Utilities to execute code blocks in Markdown files. project_name: Markdown Exec public_release: true python_package_command_line_name: '' python_package_distribution_name: markdown-exec python_package_import_name: markdown_exec repository_name: markdown-exec repository_namespace: pawamoy repository_provider: github.com markdown-exec-1.8.0/.github/000077500000000000000000000000001454604644200156355ustar00rootroot00000000000000markdown-exec-1.8.0/.github/FUNDING.yml000066400000000000000000000001071454604644200174500ustar00rootroot00000000000000github: pawamoy ko_fi: pawamoy custom: - https://www.paypal.me/pawamoy markdown-exec-1.8.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001454604644200200205ustar00rootroot00000000000000markdown-exec-1.8.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000027001454604644200225110ustar00rootroot00000000000000--- name: Bug report about: Create a bug report to help us improve. title: "bug: " labels: unconfirmed assignees: [pawamoy] --- ### Description of the bug ### To Reproduce ``` WRITE MRE / INSTRUCTIONS HERE ``` ### Full traceback
Full traceback ```python PASTE TRACEBACK HERE ```
### Expected behavior ### Environment information ```bash python -m markdown_exec.debug # | xclip -selection clipboard ``` PASTE OUTPUT HERE ### Additional context markdown-exec-1.8.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003321454604644200220060ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: I have a question / I need help url: https://github.com/pawamoy/markdown-exec/discussions/new?category=q-a about: Ask and answer questions in the Discussions tab. markdown-exec-1.8.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000012131454604644200235420ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project. title: "feature: " labels: feature assignees: pawamoy --- ### Is your feature request related to a problem? Please describe. ### Describe the solution you'd like ### Describe alternatives you've considered ### Additional context markdown-exec-1.8.0/.github/workflows/000077500000000000000000000000001454604644200176725ustar00rootroot00000000000000markdown-exec-1.8.0/.github/workflows/ci.yml000066400000000000000000000051121454604644200210070ustar00rootroot00000000000000name: ci on: push: pull_request: branches: - main defaults: run: shell: bash env: LANG: en_US.utf-8 LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up Graphviz uses: ts-graphviz/setup-graphviz@v1 - name: Set up PDM uses: pdm-project/setup-pdm@v3 with: python-version: "3.8" - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-quality - name: Install dependencies run: pdm install -G ci-quality - name: Check if the documentation builds correctly run: pdm run duty check-docs - name: Check the code quality run: pdm run duty check-quality - name: Check if the code is correctly typed run: pdm run duty check-types - name: Check for vulnerabilities in dependencies run: pdm run duty check-dependencies - name: Check for breaking changes in the API run: pdm run duty check-api exclude-test-jobs: runs-on: ubuntu-latest outputs: jobs: ${{ steps.exclude-jobs.outputs.jobs }} steps: - id: exclude-jobs run: | if ${{ github.repository_owner == 'pawamoy-insiders' }}; then echo 'jobs=[ {"os": "macos-latest"}, {"os": "windows-latest"}, {"python-version": "3.9"}, {"python-version": "3.10"}, {"python-version": "3.11"}, {"python-version": "3.12"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT else echo 'jobs=[]' >> $GITHUB_OUTPUT fi tests: needs: exclude-test-jobs strategy: max-parallel: 4 matrix: os: - ubuntu-latest - macos-latest - windows-latest python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.python-version == '3.12' }} steps: - name: Checkout uses: actions/checkout@v3 - name: Set up PDM uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} allow-python-prereleases: true - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-tests - name: Install dependencies run: pdm install --no-editable -G ci-tests - name: Run the test suite run: pdm run duty test markdown-exec-1.8.0/.github/workflows/release.yml000066400000000000000000000025551454604644200220440ustar00rootroot00000000000000name: release on: push permissions: contents: write jobs: release: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout uses: actions/checkout@v3 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Setup Python uses: actions/setup-python@v4 - name: Install build if: github.repository_owner == 'pawamoy-insiders' run: python -m pip install build - name: Build dists if: github.repository_owner == 'pawamoy-insiders' run: python -m build - name: Upload dists artifact uses: actions/upload-artifact@v3 if: github.repository_owner == 'pawamoy-insiders' with: name: markdown-exec-insiders path: ./dist/* - name: Install git-changelog if: github.repository_owner != 'pawamoy-insiders' run: pip install git-changelog - name: Prepare release notes if: github.repository_owner != 'pawamoy-insiders' run: git-changelog --release-notes > release-notes.md - name: Create release with assets uses: softprops/action-gh-release@v1 if: github.repository_owner == 'pawamoy-insiders' with: files: ./dist/* - name: Create release uses: softprops/action-gh-release@v1 if: github.repository_owner != 'pawamoy-insiders' with: body_path: release-notes.md markdown-exec-1.8.0/.gitignore000066400000000000000000000003141454604644200162630ustar00rootroot00000000000000.idea/ __pycache__/ *.py[cod] dist/ *.egg-info/ build/ htmlcov/ .coverage* pip-wheel-metadata/ .pytest_cache/ .mypy_cache/ site/ pdm.lock pdm.toml .pdm-plugins/ .pdm-python __pypackages__/ .venv/ .cache/ markdown-exec-1.8.0/.gitpod.dockerfile000066400000000000000000000001741454604644200176740ustar00rootroot00000000000000FROM gitpod/workspace-full USER gitpod ENV PIP_USER=no RUN pip3 install pipx; \ pipx install pdm; \ pipx ensurepath markdown-exec-1.8.0/.gitpod.yml000066400000000000000000000002171454604644200163640ustar00rootroot00000000000000vscode: extensions: - ms-python.python image: file: .gitpod.dockerfile ports: - port: 8000 onOpen: notify tasks: - init: make setup markdown-exec-1.8.0/CHANGELOG.md000066400000000000000000000344131454604644200161130ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [1.8.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.8.0) - 2024-01-05 [Compare with 1.7.0](https://github.com/pawamoy/markdown-exec/compare/1.7.0...1.8.0) ### Features - Add `pyodide` fence ([3a2fab0](https://github.com/pawamoy/markdown-exec/commit/3a2fab0b23196a4122bcee6d9b81d3f421f11bbb) by Timothée Mazzucotelli). - Add `ansi` option to mark ANSI extra as required or not ([27743c2](https://github.com/pawamoy/markdown-exec/commit/27743c20f56dd00ce730e1d028d362a4f95e48c7) by Timothée Mazzucotelli). [Issue #28](https://github.com/pawamoy/markdown-exec/issues/28), [Issue #29](https://github.com/pawamoy/markdown-exec/issues/29) ### Code Refactoring - Modernize MkDocs plugin ([4864608](https://github.com/pawamoy/markdown-exec/commit/48646081746c6c5ece0c6566a4b9733ace518791) by Timothée Mazzucotelli). ## [1.7.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.7.0) - 2023-10-17 [Compare with 1.6.0](https://github.com/pawamoy/markdown-exec/compare/1.6.0...1.7.0) ### Features - Set `MKDOCS_CONFIG_DIR` environment variable to build file path relative to it ([a2cbea5](https://github.com/pawamoy/markdown-exec/commit/a2cbea52d39ef43960c910830eae14dc846624d0) by Timothée Mazzucotelli). ## [1.6.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.6.0) - 2023-04-18 [Compare with 1.5.3](https://github.com/pawamoy/markdown-exec/compare/1.5.3...1.6.0) ### Features - Add `idprefix` option allowing to change/remove HTML id/href prefixes ([4d91463](https://github.com/pawamoy/markdown-exec/commit/4d914630e5642feb87103644800d3c9f7b59c6ad) by Timothée Mazzucotelli). ## [1.5.3](https://github.com/pawamoy/markdown-exec/releases/tag/1.5.3) - 2023-04-18 [Compare with 1.5.2](https://github.com/pawamoy/markdown-exec/compare/1.5.2...1.5.3) ### Code Refactoring - Reuse Markdown configuration as declared in mkdocs.yml ([afe091c](https://github.com/pawamoy/markdown-exec/commit/afe091caa33ed54fd65e25e4f90b8b60786ba3f9) by Timothée Mazzucotelli). ## [1.5.2](https://github.com/pawamoy/markdown-exec/releases/tag/1.5.2) - 2023-04-18 [Compare with 1.5.1](https://github.com/pawamoy/markdown-exec/compare/1.5.1...1.5.2) ### Code Refactoring - Reset counter in post build event ([3bf80de](https://github.com/pawamoy/markdown-exec/commit/3bf80deabe9a7438b459c73e962c9693bce71135) by Timothée Mazzucotelli). ## [1.5.1](https://github.com/pawamoy/markdown-exec/releases/tag/1.5.1) - 2023-04-17 [Compare with 1.5.0](https://github.com/pawamoy/markdown-exec/compare/1.5.0...1.5.1) ### Bug Fixes - Remove pycon output lines when rendering source as console ([fb5a23d](https://github.com/pawamoy/markdown-exec/commit/fb5a23d8d1d50aa2a1ede97150c269a07fa200ec) by Timothée Mazzucotelli). - Fix nested rendering ([a110d44](https://github.com/pawamoy/markdown-exec/commit/a110d446209b390ec8a4ad8868818352f72a9808) by Timothée Mazzucotelli). ## [1.5.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.5.0) - 2023-04-17 [Compare with 1.4.1](https://github.com/pawamoy/markdown-exec/compare/1.4.1...1.5.0) ### Features - Update ToC with generated headings ([5ea2263](https://github.com/pawamoy/markdown-exec/commit/5ea2263d53729b6d3e79da69c29b171bb6c3e22d) by Timothée Mazzucotelli). ## [1.4.1](https://github.com/pawamoy/markdown-exec/releases/tag/1.4.1) - 2023-04-16 [Compare with 1.4.0](https://github.com/pawamoy/markdown-exec/compare/1.4.0...1.4.1) ### Bug Fixes - Improve handling of errors within sessions ([87ac5f3](https://github.com/pawamoy/markdown-exec/commit/87ac5f352ce44370f52a7fb56d846c04b76447f9) by Timothée Mazzucotelli). - Swallow non-extra parameters in run functions ([f5d4fef](https://github.com/pawamoy/markdown-exec/commit/f5d4fef1f78d94c3f8850f873076e3cd68c0a981) by Timothée Mazzucotelli). ### Code Refactoring - Simplify tree formatter signature ([09d5427](https://github.com/pawamoy/markdown-exec/commit/09d542772ccb0d1250366b39fa3a9c9362e1ed42) by Timothée Mazzucotelli). ## [1.4.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.4.0) - 2023-03-15 [Compare with 1.3.0](https://github.com/pawamoy/markdown-exec/compare/1.3.0...1.4.0) ### Features - Sessions: persist and reuse state for Python and Pycon code blocks ([a8fef5e](https://github.com/pawamoy/markdown-exec/commit/a8fef5e90b1d7165e16ff5afe4b84e8441503098) by Timothée Mazzucotelli). [Issue #16](https://github.com/pawamoy/markdown-exec/issues/16) ## [1.3.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.3.0) - 2023-02-18 [Compare with 1.2.0](https://github.com/pawamoy/markdown-exec/compare/1.2.0...1.3.0) ### Features - Support wrapping result with console source ([268c82e](https://github.com/pawamoy/markdown-exec/commit/268c82e6f005dcaa1ddc75608d2f28927f069761) by Timothée Mazzucotelli). [Issue #13](https://github.com/pawamoy/markdown-exec/issues/13) ### Code Refactoring - Remove margin hack from Material source ([beec237](https://github.com/pawamoy/markdown-exec/commit/beec2374b27075e66ddb4a7cdc2f2c81b7455b95) by Timothée Mazzucotelli). - Better support pycon syntax ([22b51c6](https://github.com/pawamoy/markdown-exec/commit/22b51c64155060922e46ea10e6c0d1c1c1b00a2f) by Timothée Mazzucotelli). ## [1.2.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.2.0) - 2023-02-01 [Compare with 1.1.0](https://github.com/pawamoy/markdown-exec/compare/1.1.0...1.2.0) ### Features - Support ANSI code blocks ([39719c5](https://github.com/pawamoy/markdown-exec/commit/39719c5d7ac1bbde6d60002082a0ad3b48730545) by Timothée Mazzucotelli). [Issue #11](https://github.com/pawamoy/markdown-exec/issues/11) ## [1.1.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.1.0) - 2023-01-27 [Compare with 1.0.0](https://github.com/pawamoy/markdown-exec/compare/1.0.0...1.1.0) ### Features - Log details to help debugging errors ([4c0228d](https://github.com/pawamoy/markdown-exec/commit/4c0228da41f5970e719b20a40c0fab47a9d12244) by Timothée Mazzucotelli). [Issue #12](https://github.com/pawamoy/markdown-exec/issues/12) - Allow expecting specific exit codes ([620ec66](https://github.com/pawamoy/markdown-exec/commit/620ec66182dd0f84600258408720779822615085) by Timothée Mazzucotelli). [Issue #10](https://github.com/pawamoy/markdown-exec/issues/10) ### Code Refactoring - Formatters now only accept keyword arguments ([0940ca9](https://github.com/pawamoy/markdown-exec/commit/0940ca98e81548474351e234715df2fc290fdc1e) by Timothée Mazzucotelli). ## [1.0.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.0.0) - 2022-11-24 [Compare with 0.7.4](https://github.com/pawamoy/markdown-exec/compare/0.7.4...1.0.0) ### Features - Allow defining IDs on code blocks (for warnings) ([0091167](https://github.com/pawamoy/markdown-exec/commit/009116719e81dd91190b391c82709fb179a62364) by Timothée Mazzucotelli). ### Code Refactoring - Use base format everywhere (more flexible) ([cefba70](https://github.com/pawamoy/markdown-exec/commit/cefba704ae45df1b115b969e3d4d5105ebd052dd) by Timothée Mazzucotelli). ## [0.7.4](https://github.com/pawamoy/markdown-exec/releases/tag/0.7.4) - 2022-11-13 [Compare with 0.7.3](https://github.com/pawamoy/markdown-exec/compare/0.7.3...0.7.4) ### Bug Fixes - Render source for non-HTML output (regression) ([3028dcd](https://github.com/pawamoy/markdown-exec/commit/3028dcd4f20f94b578995c326fd68d53a6dc3638) by Timothée Mazzucotelli). ## [0.7.3](https://github.com/pawamoy/markdown-exec/releases/tag/0.7.3) - 2022-11-13 [Compare with 0.7.2](https://github.com/pawamoy/markdown-exec/compare/0.7.2...0.7.3) ### Bug Fixes - Don't wrap HTML in `p` tag ([420d79d](https://github.com/pawamoy/markdown-exec/commit/420d79d67c2a6bdc925b3bc3d89790258f922317) by Timothée Mazzucotelli). ## [0.7.2](https://github.com/pawamoy/markdown-exec/releases/tag/0.7.2) - 2022-09-01 [Compare with 0.7.1](https://github.com/pawamoy/markdown-exec/compare/0.7.1...0.7.2) ### Bug Fixes - Make `tree` formatter forward extra options ([54996a9](https://github.com/pawamoy/markdown-exec/commit/54996a9bc2c803bb8c9de0861af69723ddb000fa) by Timothée Mazzucotelli). - Fix race condition issue ([37d7f86](https://github.com/pawamoy/markdown-exec/commit/37d7f86eeaa73029ae89c1c5d07146d2387b10d3) by Timothée Mazzucotelli). ## [0.7.1](https://github.com/pawamoy/markdown-exec/releases/tag/0.7.1) - 2022-08-28 [Compare with 0.7.0](https://github.com/pawamoy/markdown-exec/compare/0.7.0...0.7.1) ### Bug Fixes - Allow printing non-string objects ([ceaa482](https://github.com/pawamoy/markdown-exec/commit/ceaa482d16adfbd1609595a2ed6a241bad71f9de) by Timothée Mazzucotelli). [Issue #7](https://github.com/pawamoy/markdown-exec/issues/7) ## [0.7.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.7.0) - 2022-05-28 [Compare with 0.6.0](https://github.com/pawamoy/markdown-exec/compare/0.6.0...0.7.0) ### Features - Add ability to hide source lines ([3cb1934](https://github.com/pawamoy/markdown-exec/commit/3cb19345fa2b65478ac439b5f486d04bf5ff5337) by Timothée Mazzucotelli). ## [0.6.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.6.0) - 2022-05-21 [Compare with 0.5.0](https://github.com/pawamoy/markdown-exec/compare/0.5.0...0.6.0) ### Features - Add tree formatter ([8096990](https://github.com/pawamoy/markdown-exec/commit/8096990dcbf6795572e5e5afee12195d5a56c6f6) by Timothée Mazzucotelli). - Handle code blocks execution errors and log warnings ([34e16db](https://github.com/pawamoy/markdown-exec/commit/34e16db679721db7d1df375912d512b5aed80b1a) by Timothée Mazzucotelli). ### Bug Fixes - Fix Python execution to support nested scopes ([74b9a95](https://github.com/pawamoy/markdown-exec/commit/74b9a95ade3862752fb78d6c64be8b9b1d4d3886) by Timothée Mazzucotelli). ## [0.5.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.5.0) - 2022-05-09 [Compare with 0.4.0](https://github.com/pawamoy/markdown-exec/compare/0.4.0...0.5.0) ### Features - Allow wrapping result in code block ([37201e4](https://github.com/pawamoy/markdown-exec/commit/37201e4409badec903f311bcc0a6ab7acddff37c) by Timothée Mazzucotelli). - Add support for shell code blocks ([f2b4b67](https://github.com/pawamoy/markdown-exec/commit/f2b4b671f4399637d0dac235a0af7739033f9526) by Timothée Mazzucotelli). ### Code Refactoring - Fetch plugin languages from dict ([de8309e](https://github.com/pawamoy/markdown-exec/commit/de8309e6895a079031461bfea317215bcff9bc21) by Timothée Mazzucotelli). - Add reusable base formatter ([c265bee](https://github.com/pawamoy/markdown-exec/commit/c265bee9abf0ad545b7fdc6ccf2e320071295a18) by Timothée Mazzucotelli). ## [0.4.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.4.0) - 2022-05-09 [Compare with 0.3.1](https://github.com/pawamoy/markdown-exec/compare/0.3.1...0.4.0) ### Features - Add literate Markdown support ([8d3ed7e](https://github.com/pawamoy/markdown-exec/commit/8d3ed7ef5c9a88849be0a5da44e7b478eb44c180) by Timothée Mazzucotelli). - Add `material-block` style to show source ([ff10ee1](https://github.com/pawamoy/markdown-exec/commit/ff10ee1f0b55b2e77b97f272b49b24024f9de2ac) by Timothée Mazzucotelli). - Support up to 8 levels of exec code block nesting ([bfde808](https://github.com/pawamoy/markdown-exec/commit/bfde8087ca6f4eb91aba8eb01b37755dfacb4cdb) by Timothée Mazzucotelli). ## [0.3.1](https://github.com/pawamoy/markdown-exec/releases/tag/0.3.1) - 2022-05-07 [Compare with 0.3.0](https://github.com/pawamoy/markdown-exec/compare/0.3.0...0.3.1) ### Bug Fixes - Actually prevent HTML re-rendering ([4374852](https://github.com/pawamoy/markdown-exec/commit/4374852706207beac3b8dbd8dc9d75be51b1df0d) by Timothée Mazzucotelli). ## [0.3.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.3.0) - 2022-05-01 [Compare with 0.2.0](https://github.com/pawamoy/markdown-exec/compare/0.2.0...0.3.0) ### Features - Support `pycon` code blocks ([2c86394](https://github.com/pawamoy/markdown-exec/commit/2c86394417858654af21316c3555aff0e9fd2d26) by Timothée Mazzucotelli). - Add `console` source integration option ([62dfd74](https://github.com/pawamoy/markdown-exec/commit/62dfd74185f7f33cdf6f4726b3aa898a1ac5d22f) by Timothée Mazzucotelli). - Provide a MkDocs plugin for easier setup ([5fce814](https://github.com/pawamoy/markdown-exec/commit/5fce81462063b7c61d9833939e44958a466d4b24) by Timothée Mazzucotelli). - Support changing tabs titles ([d150596](https://github.com/pawamoy/markdown-exec/commit/d150596beda1e5a5304bc06e27668294a75ff220) by Timothée Mazzucotelli). - Allow using `print` in code blocks ([7c124fd](https://github.com/pawamoy/markdown-exec/commit/7c124fd416d6923bea2834479d972b14c3e22112) by Timothée Mazzucotelli). - Allow passing extra opts like title to source code blocks ([bb3252a](https://github.com/pawamoy/markdown-exec/commit/bb3252a3e959cea198966ba59a70f6f5aa57a963) by Timothée Mazzucotelli). ### Code Refactoring - Split Python formatter to allow reuse ([fc56702](https://github.com/pawamoy/markdown-exec/commit/fc56702b9c393323adc30abba42c823f601ef738) by Timothée Mazzucotelli). - Setup a more robust Markdown converter ([395f4c4](https://github.com/pawamoy/markdown-exec/commit/395f4c4c21ab7f4afcc88250c1fd882269a06d02) by Timothée Mazzucotelli). ## [0.2.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.2.0) - 2022-04-18 [Compare with 0.1.0](https://github.com/pawamoy/markdown-exec/compare/0.1.0...0.2.0) ### Features - Add ability to render using tabs ([91a95ae](https://github.com/pawamoy/markdown-exec/commit/91a95ae4c6ad82e85dac24a110d09ca71eff688a) by Timothée Mazzucotelli). ## [0.1.0](https://github.com/pawamoy/markdown-exec/releases/tag/0.1.0) - 2022-02-19 [Compare with first commit](https://github.com/pawamoy/markdown-exec/compare/41c8d81992d2443cd5c3418df0f461b0af1a6ec8...0.1.0) ### Features - Implement execution of code blocks ([41c8d81](https://github.com/pawamoy/markdown-exec/commit/41c8d81992d2443cd5c3418df0f461b0af1a6ec8) by Timothée Mazzucotelli). markdown-exec-1.8.0/CODE_OF_CONDUCT.md000066400000000000000000000125471454604644200171050ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at pawamoy@pm.me. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations markdown-exec-1.8.0/CONTRIBUTING.md000066400000000000000000000102411454604644200165240ustar00rootroot00000000000000# Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ## Environment setup Nothing easier! Fork and clone the repository, then: ```bash cd markdown-exec make setup ``` > NOTE: > If it fails for some reason, > you'll need to install > [PDM](https://github.com/pdm-project/pdm) > manually. > > You can install it with: > > ```bash > python3 -m pip install --user pipx > pipx install pdm > ``` > > Now you can try running `make setup` again, > or simply `pdm install`. You now have the dependencies installed. Run `make help` to see all the available actions! ## Tasks This project uses [duty](https://github.com/pawamoy/duty) to run tasks. A Makefile is also provided. The Makefile will try to run certain tasks on multiple Python versions. If for some reason you don't want to run the task on multiple Python versions, you run the task directly with `pdm run duty TASK`. The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-pdm/work/#vscode-setup) for the project. ## Development As usual: 1. create a new branch: `git switch -c feature-or-bugfix-name` 1. edit the code and/or the documentation **Before committing:** 1. run `make format` to auto-format the code 1. run `make check` to check everything (fix any warning) 1. run `make test` to run the tests (fix any issue) 1. if you updated the documentation or the project dependencies: 1. run `make docs` 1. go to http://localhost:8000 and check that everything looks good 1. follow our [commit message convention](#commit-message-convention) If you are unsure about how to fix or ignore a warning, just let the continuous integration fail, and we will help you during review. Don't bother updating the changelog, we will take care of this. ## Commit message convention Commit messages must follow our convention based on the [Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html): ``` [(scope)]: Subject [Body] ``` **Subject and body must be valid Markdown.** Subject must have proper casing (uppercase for first letter if it makes sense), but no dot at the end, and no punctuation in general. Scope and body are optional. Type can be: - `build`: About packaging, building wheels, etc. - `chore`: About packaging or repo/files management. - `ci`: About Continuous Integration. - `deps`: Dependencies update. - `docs`: About documentation. - `feat`: New feature. - `fix`: Bug fix. - `perf`: About performance. - `refactor`: Changes that are not features or bug fixes. - `style`: A change in code style/format. - `tests`: About tests. If you write a body, please add trailers at the end (for example issues and PR references, or co-authors), without relying on GitHub's flavored Markdown: ``` Body. Issue #10: https://github.com/namespace/project/issues/10 Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15 ``` These "trailers" must appear at the end of the body, without any blank lines between them. The trailer title can contain any character except colons `:`. We expect a full URI for each trailer, not just GitHub autolinks (for example, full GitHub URLs for commits and issues, not the hash or the #issue-number). We do not enforce a line length on commit messages summary and body, but please avoid very long summaries, and very long lines in the body, unless they are part of code blocks that must not be wrapped. ## Pull requests guidelines Link to any related issue in the Pull Request message. During the review, we recommend using fixups: ```bash # SHA is the SHA of the commit you want to fix git commit --fixup=SHA ``` Once all the changes are approved, you can squash your commits: ```bash git rebase -i --autosquash main ``` And force-push: ```bash git push -f ``` If this seems all too complicated, you can push or force-push each new commit, and we will squash them ourselves if needed, before merging. markdown-exec-1.8.0/LICENSE000066400000000000000000000013621454604644200153040ustar00rootroot00000000000000ISC License Copyright (c) 2022, Timothée Mazzucotelli Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. markdown-exec-1.8.0/Makefile000066400000000000000000000016101454604644200157330ustar00rootroot00000000000000.DEFAULT_GOAL := help SHELL := bash DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_quality_args = files docs_args = host port release_args = version test_args = match BASIC_DUTIES = \ changelog \ check-api \ check-dependencies \ clean \ coverage \ docs \ docs-deploy \ format \ release \ vscode QUALITY_DUTIES = \ check-quality \ check-docs \ check-types \ test .PHONY: help help: @$(DUTY) --list .PHONY: lock lock: @pdm lock -G:all .PHONY: setup setup: @bash scripts/setup.sh .PHONY: check check: @pdm multirun duty check-quality check-types check-docs @$(DUTY) check-dependencies check-api .PHONY: $(BASIC_DUTIES) $(BASIC_DUTIES): @$(DUTY) $@ $(call args,$@) .PHONY: $(QUALITY_DUTIES) $(QUALITY_DUTIES): @pdm multirun duty $@ $(call args,$@) markdown-exec-1.8.0/README.md000066400000000000000000000064471454604644200155670ustar00rootroot00000000000000# Markdown Exec [![ci](https://github.com/pawamoy/markdown-exec/workflows/ci/badge.svg)](https://github.com/pawamoy/markdown-exec/actions?query=workflow%3Aci) [![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://pawamoy.github.io/markdown-exec/) [![pypi version](https://img.shields.io/pypi/v/markdown-exec.svg)](https://pypi.org/project/markdown-exec/) [![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/markdown-exec) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#markdown-exec:gitter.im) Utilities to execute code blocks in Markdown files. For example, you write a Python code block that computes some HTML, and this HTML is injected in place of the code block. ## Installation With `pip`: ```bash pip install markdown-exec[ansi] ``` The `ansi` extra provides the necessary bits (`pygments-ansi-color` and a CSS file) to render ANSI colors in HTML code blocks. The CSS file is automatically added to MkDocs' `extra_css` when Markdown Exec is activated via `plugins` (see below). ## Configuration This extension relies on the [SuperFences](https://facelessuser.github.io/pymdown-extensions/extensions/superfences/) extension of [PyMdown Extensions](https://facelessuser.github.io/pymdown-extensions/). To allow execution of code blocks, configure a custom fence from Python: ```python from markdown import Markdown from markdown_exec import formatter, validator Markdown( extensions=["pymdownx.superfences"], extension_configs={ "pymdownx.superfences": { "custom_fences": [ { "name": "python", "class": "python", "validator": validator, "format": formatter, } # ...one fence for each language we support: # bash, console, md, markdown, py, python, pycon, sh, tree ] } } ) ``` ...or in MkDocs configuration file, as a Markdown extension: ```yaml # mkdocs.yml markdown_extensions: - pymdownx.superfences: custom_fences: - name: python class: python validator: !!python/name:markdown_exec.validator format: !!python/name:markdown_exec.formatter # ...one fence for each language we support: # bash, console, md, markdown, py, python, pycon, sh, tree ``` ...or in MkDocs configuration file, as a plugin: ```yaml # mkdocs.yml plugins: - search - markdown-exec ``` We do recommend enabling Markdown Exec with the MkDocs plugin if you are using MkDocs: it will take care of adding relevant assets (CSS/JS) to the final site when needed. ## Usage You are now able to execute code blocks instead of displaying them: ````md ```python exec="on" print("Hello Markdown!") ``` ```` The `exec` option will be true for every possible value except `0`, `no`, `off` and `false` (case insensitive). Below you can see an example of running a bash script that is expected to return a non-zero exit code: ````md ```bash exec="1" source="tabbed-left" returncode="2" grep extra_css README.md && exit 2 ``` ```` See [usage](https://pawamoy.github.io/markdown-exec/usage/) for more details, and the [gallery](https://pawamoy.github.io/markdown-exec/gallery/) for more examples! markdown-exec-1.8.0/config/000077500000000000000000000000001454604644200155425ustar00rootroot00000000000000markdown-exec-1.8.0/config/black.toml000066400000000000000000000000721454604644200175120ustar00rootroot00000000000000[tool.black] line-length = 120 exclude = "tests/fixtures" markdown-exec-1.8.0/config/coverage.ini000066400000000000000000000005101454604644200200320ustar00rootroot00000000000000[coverage:run] branch = true parallel = true source = src/ tests/ [coverage:paths] equivalent = src/ __pypackages__/ [coverage:report] precision = 2 omit = src/*/__init__.py src/*/__main__.py tests/__init__.py exclude_lines = pragma: no cover if TYPE_CHECKING [coverage:json] output = htmlcov/coverage.json markdown-exec-1.8.0/config/git-changelog.toml000066400000000000000000000003121454604644200211430ustar00rootroot00000000000000bump = "auto" convention = "angular" in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" markdown-exec-1.8.0/config/mypy.ini000066400000000000000000000002701454604644200172400ustar00rootroot00000000000000[mypy] ignore_missing_imports = true exclude = (?x)( tests/fixtures/ | docs/snippets/gallery/ | docs/snippets/usage/ ) warn_unused_ignores = true show_error_codes = true markdown-exec-1.8.0/config/pytest.ini000066400000000000000000000005471454604644200176010ustar00rootroot00000000000000[pytest] norecursedirs = .git .tox .env dist build python_files = test_*.py *_test.py tests.py addopts = --cov --cov-config config/coverage.ini testpaths = tests # action:message_regex:warning_class:module_regex:line filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist markdown-exec-1.8.0/config/ruff.toml000066400000000000000000000040621454604644200174030ustar00rootroot00000000000000target-version = "py38" line-length = 132 exclude = [ "fixtures", "site", ] select = [ "A", "ANN", "ARG", "B", "BLE", "C", "C4", "COM", "D", "DTZ", "E", "ERA", "EXE", "F", "FBT", "G", "I", "ICN", "INP", "ISC", "N", "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", "Q", "RUF", "RSE", "RET", "S", "SIM", "SLF", "T", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT", ] ignore = [ "A001", # Variable is shadowing a Python builtin "ANN101", # Missing type annotation for self "ANN102", # Missing type annotation for cls "ANN204", # Missing return type annotation for special method __str__ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "ARG005", # Unused lambda argument "C901", # Too complex "D105", # Missing docstring in magic method "D417", # Missing argument description in the docstring "E501", # Line too long "ERA001", # Commented out code "G004", # Logging statement uses f-string "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "SLF001", # Private member accessed "TRY003", # Avoid specifying long messages outside the exception class ] [per-file-ignores] "src/*/cli.py" = [ "T201", # Print statement ] "src/*/debug.py" = [ "T201", # Print statement ] "scripts/*.py" = [ "INP001", # File is part of an implicit namespace package "T201", # Print statement ] "tests/*.py" = [ "ARG005", # Unused lambda argument "FBT001", # Boolean positional arg in function definition "PLR2004", # Magic value used in comparison "S101", # Use of assert detected ] [flake8-quotes] docstring-quotes = "double" [flake8-tidy-imports] ban-relative-imports = "all" [isort] known-first-party = ["markdown_exec"] [pydocstyle] convention = "google" markdown-exec-1.8.0/config/vscode/000077500000000000000000000000001454604644200170255ustar00rootroot00000000000000markdown-exec-1.8.0/config/vscode/launch.json000066400000000000000000000015711454604644200211760ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "python (current file)", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false }, { "name": "test", "type": "python", "request": "launch", "module": "pytest", "justMyCode": false, "args": [ "-c=config/pytest.ini", "-vvv", "--no-cov", "--dist=no", "tests", "-k=${input:tests_selection}" ] } ], "inputs": [ { "id": "tests_selection", "type": "promptString", "description": "Tests selection", "default": "" } ] }markdown-exec-1.8.0/config/vscode/settings.json000066400000000000000000000030061454604644200215570ustar00rootroot00000000000000{ "files.watcherExclude": { "**/__pypackages__/**": true, "**/.venv*/**": true, "**/venv*/**": true }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }, "python.autoComplete.extraPaths": [ "__pypackages__/3.8/lib", "__pypackages__/3.9/lib", "__pypackages__/3.10/lib", "__pypackages__/3.11/lib", "__pypackages__/3.12/lib" ], "python.analysis.extraPaths": [ "__pypackages__/3.8/lib", "__pypackages__/3.9/lib", "__pypackages__/3.10/lib", "__pypackages__/3.11/lib", "__pypackages__/3.12/lib" ], "black-formatter.args": [ "--config=config/black.toml" ], "mypy-type-checker.args": [ "--config-file=config/mypy.ini" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.pytestArgs": [ "--config-file=config/pytest.ini" ], "ruff.format.args": [ "--config=config/ruff.toml" ], "ruff.lint.args": [ "--config=config/ruff.toml" ], "yaml.schemas": { "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" }, "yaml.customTags": [ "!ENV scalar", "!ENV sequence", "!relative scalar", "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" ] }markdown-exec-1.8.0/config/vscode/tasks.json000066400000000000000000000044261454604644200210530ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "changelog", "type": "shell", "command": "pdm run duty changelog" }, { "label": "check", "type": "shell", "command": "pdm run duty check" }, { "label": "check-quality", "type": "shell", "command": "pdm run duty check-quality" }, { "label": "check-types", "type": "shell", "command": "pdm run duty check-types" }, { "label": "check-docs", "type": "shell", "command": "pdm run duty check-docs" }, { "label": "check-dependencies", "type": "shell", "command": "pdm run duty check-dependencies" }, { "label": "check-api", "type": "shell", "command": "pdm run duty check-api" }, { "label": "clean", "type": "shell", "command": "pdm run duty clean" }, { "label": "docs", "type": "shell", "command": "pdm run duty docs" }, { "label": "docs-deploy", "type": "shell", "command": "pdm run duty docs-deploy" }, { "label": "format", "type": "shell", "command": "pdm run duty format" }, { "label": "lock", "type": "shell", "command": "pdm lock -G:all" }, { "label": "release", "type": "shell", "command": "pdm run duty release ${input:version}" }, { "label": "setup", "type": "shell", "command": "bash scripts/setup.sh" }, { "label": "test", "type": "shell", "command": "pdm run duty test coverage", "group": "test" }, { "label": "vscode", "type": "shell", "command": "pdm run duty vscode" } ], "inputs": [ { "id": "version", "type": "promptString", "description": "Version" } ] }markdown-exec-1.8.0/docs/000077500000000000000000000000001454604644200152255ustar00rootroot00000000000000markdown-exec-1.8.0/docs/.overrides/000077500000000000000000000000001454604644200173055ustar00rootroot00000000000000markdown-exec-1.8.0/docs/.overrides/main.html000066400000000000000000000010471454604644200211210ustar00rootroot00000000000000{% extends "base.html" %} {% block announce %} Sponsorship is now available! {% include ".icons/octicons/heart-fill-16.svg" %} — For updates follow @pawamoy on {% include ".icons/fontawesome/brands/mastodon.svg" %} Fosstodon {% endblock %} markdown-exec-1.8.0/docs/changelog.md000066400000000000000000000000261454604644200174740ustar00rootroot00000000000000--8<-- "CHANGELOG.md" markdown-exec-1.8.0/docs/code_of_conduct.md000066400000000000000000000000341454604644200206610ustar00rootroot00000000000000--8<-- "CODE_OF_CONDUCT.md" markdown-exec-1.8.0/docs/contributing.md000066400000000000000000000000311454604644200202500ustar00rootroot00000000000000--8<-- "CONTRIBUTING.md" markdown-exec-1.8.0/docs/credits.md000066400000000000000000000002011454604644200171750ustar00rootroot00000000000000--- hide: - toc --- ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` markdown-exec-1.8.0/docs/css/000077500000000000000000000000001454604644200160155ustar00rootroot00000000000000markdown-exec-1.8.0/docs/css/insiders.css000066400000000000000000000037311454604644200203530ustar00rootroot00000000000000@keyframes heart { 0%, 40%, 80%, 100% { transform: scale(1); } 20%, 60% { transform: scale(1.15); } } @keyframes vibrate { 0%, 2%, 4%, 6%, 8%, 10%, 12%, 14%, 16%, 18% { -webkit-transform: translate3d(-2px, 0, 0); transform: translate3d(-2px, 0, 0); } 1%, 3%, 5%, 7%, 9%, 11%, 13%, 15%, 17%, 19% { -webkit-transform: translate3d(2px, 0, 0); transform: translate3d(2px, 0, 0); } 20%, 100% { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .heart { color: #e91e63; } .pulse { animation: heart 1000ms infinite; } .vibrate { animation: vibrate 2000ms infinite; } .new-feature svg { fill: var(--md-accent-fg-color) !important; } a.insiders { color: #e91e63; } .sponsorship-list { width: 100%; } .sponsorship-item { border-radius: 100%; display: inline-block; height: 1.6rem; margin: 0.1rem; overflow: hidden; width: 1.6rem; } .sponsorship-item:focus, .sponsorship-item:hover { transform: scale(1.1); } .sponsorship-item img { filter: grayscale(100%) opacity(75%); height: auto; width: 100%; } .sponsorship-item:focus img, .sponsorship-item:hover img { filter: grayscale(0); } .sponsorship-item.private { background: var(--md-default-fg-color--lightest); color: var(--md-default-fg-color); font-size: .6rem; font-weight: 700; line-height: 1.6rem; text-align: center; } .mastodon { color: #897ff8; border-radius: 100%; box-shadow: inset 0 0 0 .05rem currentcolor; display: inline-block; height: 1.2rem !important; padding: .25rem; transition: all .25s; vertical-align: bottom !important; width: 1.2rem; } .premium-sponsors { text-align: center; } #silver-sponsors img { height: 140px; } #bronze-sponsors img { height: 140px; } #bronze-sponsors p { display: flex; flex-wrap: wrap; justify-content: center; } #bronze-sponsors a { display: block; flex-shrink: 0; } .sponsors-total { font-weight: bold; }markdown-exec-1.8.0/docs/css/material.css000066400000000000000000000001311454604644200203200ustar00rootroot00000000000000/* More space at the bottom of the page. */ .md-main__inner { margin-bottom: 1.5rem; } markdown-exec-1.8.0/docs/css/mkdocstrings.css000066400000000000000000000021171454604644200212370ustar00rootroot00000000000000/* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; border-left: .05rem solid var(--md-typeset-table-color); } /* Mark external links as such. */ a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ mask-image: url('data:image/svg+xml,'); -webkit-mask-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; vertical-align: middle; position: relative; height: 1em; width: 1em; background-color: var(--md-typeset-a-color); } a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); }markdown-exec-1.8.0/docs/gallery.md000066400000000000000000000137511454604644200172150ustar00rootroot00000000000000--- hide: - navigation --- # Gallery Welcome to our gallery of examples! ## Diagrams (cloud/system architecture) [Diagrams](https://github.com/mingrammer/diagrams) offers a nice way of building diagrams. It also bundles a number of images used to illustrate objects and concepts so you can build good-looking diagrams. By default, Diagrams tries to write the result on disk, so we prevent that by patching its `render` method, and by ignoring the `FileNotFoundError` that ensues. Then we use its internal `dot` object and its `pipe` method to store the diagram in a variable, as base64 encoded PNG data. Finally we output an HTML image with the base64 data. Using SVG is not possible here since Diagrams embeds actual, smaller PNG files in the result, files which are not automatically added to the final site. ```python exec="true" html="true" source="tabbed-right" title="Diagrams" --8<-- "gallery/diagrams.py" ``` ## Python dependency tree [pipdeptree](https://github.com/tox-dev/pipdeptree) is able to output a Mermaid diagram of your Python dependency tree. In this example we change the direction of the graph from top-down to left-right, and remove local version identifiers from our own package. ````md exec="1" source="tabbed-right" title="pipdeptree mermaid diagram" ```bash exec="1" result="mermaid" pipdeptree -p markdown-exec --mermaid 2>/dev/null | sed 's/flowchart TD/flowchart LR/' | sed 's/\.dev.+"\]$/"]/;s/\+d.*"\]$/"]/' ``` ```` Another example with more dependencies and top-down direction: ````md exec="1" source="tabbed-right" title="pipdeptree mermaid diagram" ```bash exec="1" result="mermaid" pipdeptree -p mkdocstrings-python --mermaid 2>/dev/null ``` ```` ## Python modules inter-dependencies This example uses [pydeps](https://github.com/thebjorn/pydeps) to build a graph of interdependencies of your project's modules. Data is built and stored in a pydeps data structure, then translated to `dot` source, then rendered to SVG with [Graphviz](https://graphviz.org/). In this example we also add links to the code reference in related nodes. Try clicking on the `markdown_exec` nodes! NOTE: pydeps wasn't designed to be used in such a programatic way, so the code is a bit convoluted, but you could make a function of it, put it in an importable script/module, and reuse it cleanly in your executed code blocks. ```python exec="true" html="true" source="tabbed-right" title="pydeps module dependencies graph" --8<-- "gallery/pydeps.py" ``` ## Code snippets [Rich](https://github.com/Textualize/rich) allows to export syntax-highlighted code as SVG. Here we hardcode the code snippet we want to render, but we could instead include it from somewhere else using the [`pymdownx.snippets` extension](https://facelessuser.github.io/pymdown-extensions/extensions/snippets/) or by reading it dynamically from Python. We also prevent Rich from actually writing to the terminal. ```python exec="true" html="true" source="tabbed-right" title="Rich SVG code snippet" --8<-- "gallery/rich.py" ``` ## Terminal output with colors If you installed Markdown Exec with the `ansi` extra (`pip install markdown-exec[ansi]`), the ANSI colors in the output of shell commands will be translated to HTML/CSS, allowing to render them naturally in your documentation pages. For this to happen, use the [`result="ansi"` option](http://localhost:8000/markdown-exec/usage/#wrap-result-in-a-code-block). ```bash exec="true" source="tabbed-right" title="ANSI terminal output" result="ansi" --8<-- "gallery/ansi.sh" ``` As an alternative, we can use Rich again to render the output of a command in a terminal, with colors. This example is taken directly from the documentation of the [Griffe](https://github.com/mkdocstrings/griffe) project. ```python exec="true" html="true" source="tabbed-right" title="Rich terminal output" --8<-- "gallery/rich_terminal.py" ``` ## TUI screenshots [Textual](https://github.com/Textualize/textual) allows to build Terminal User Interfaces (TUIs). In this example we generate the SVG image of a terminal interface. ```python exec="1" html="true" source="tabbed-right" tabs="Source|Result" --8<-- "gallery/textual.py" ``` ## Charts and Plots With [Matplotlib](https://matplotlib.org/): ```python exec="1" html="1" source="tabbed-right" title="matplotlib graph" --8<-- "gallery/matplotlib.py" ``` ## Python module output This example uses Python's [`runpy`][runpy] module to run another Python module. This other module's output is captured by temporarily patching `sys.stdout` with a text buffer. ```python exec="true" source="tabbed-right" title="runpy and script/module output" --8<-- "gallery/runpy.py" ``` ## Python CLI documentation ### Argparse help message (code block) Instead of blindly running a module with `runpy` to get its help message, if you know the project is using [`argparse`][argparse] to build its command line interface, and if it exposes its parser, then you can get the help message directly from the parser. ```python exec="true" source="tabbed-right" title="argparse parser help message" --8<-- "gallery/argparse_format.py" ``` ### Argparse parser documentation In this example, we inspect the `argparse` parser to build better-looking Markdown/HTML contents. We simply use the description and iterate on options, but more complex stuff is possible of course. ```python exec="true" source="tabbed-right" updatetoc="no" title="CLI help using argparse parser" --8<-- "gallery/argparse.py" ``` markdown-exec-1.8.0/docs/index.md000066400000000000000000000000231454604644200166510ustar00rootroot00000000000000--8<-- "README.md" markdown-exec-1.8.0/docs/insiders/000077500000000000000000000000001454604644200170455ustar00rootroot00000000000000markdown-exec-1.8.0/docs/insiders/changelog.md000066400000000000000000000004611454604644200213170ustar00rootroot00000000000000# Changelog ## Markdown Exec Insiders ### 1.0.1 June 15, 2023 { id="1.0.1" } - Support HTML minification by wrapping code in pre tags - Catch JS error on pages without Pyodide fences ### 1.0.0 April 26, 2023 { id="1.0.0" } - Add a [`pyodide` fence](../usage/pyodide.md) markdown-exec-1.8.0/docs/insiders/goals.yml000066400000000000000000000002101454604644200206660ustar00rootroot00000000000000goals: 500: name: PlasmaVac User Guide features: - name: Pyodide fence ref: /usage/pyodide/ since: 2023/04/26 markdown-exec-1.8.0/docs/insiders/index.md000066400000000000000000000220461454604644200205020ustar00rootroot00000000000000# Insiders *Markdown Exec* follows the **sponsorware** release strategy, which means that new features are first exclusively released to sponsors as part of [Insiders][insiders]. Read on to learn [what sponsorships achieve][sponsorship], [how to become a sponsor][sponsors] to get access to Insiders, and [what's in it for you][features]! ## What is Insiders? *Markdown Exec Insiders* is a private fork of *Markdown Exec*, hosted as a private GitHub repository. Almost[^1] [all new features][features] are developed as part of this fork, which means that they are immediately available to all eligible sponsors, as they are made collaborators of this repository. [^1]: In general, every new feature is first exclusively released to sponsors, but sometimes upstream dependencies enhance existing features that must be supported by *Markdown Exec*. Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a funding goal is hit, the features that are tied to it are merged back into *Markdown Exec* and released for general availability, making them available to all users. Bugfixes are always released in tandem. Sponsorships start as low as [**$10 a month**][sponsors].[^2] [^2]: Note that $10 a month is the minimum amount to become eligible for Insiders. While GitHub Sponsors also allows to sponsor lower amounts or one-time amounts, those can't be granted access to Insiders due to technical reasons. Such contributions are still very much welcome as they help ensuring the project's sustainability. ## What sponsorships achieve Sponsorships make this project sustainable, as they buy the maintainers of this project time – a very scarce resource – which is spent on the development of new features, bug fixing, stability improvement, issue triage and general support. The biggest bottleneck in Open Source is time.[^3] [^3]: Making an Open Source project sustainable is exceptionally hard: maintainers burn out, projects are abandoned. That's not great and very unpredictable. The sponsorware model ensures that if you decide to use *Markdown Exec*, you can be sure that bugs are fixed quickly and new features are added regularly. If you're unsure if you should sponsor this project, check out the list of [completed funding goals][goals completed] to learn whether you're already using features that were developed with the help of sponsorships. You're most likely using at least a handful of them, [thanks to our awesome sponsors][sponsors]! ## What's in it for me? ```python exec="1" session="insiders" data_source = "docs/insiders/goals.yml" ``` ```python exec="1" session="insiders" --8<-- "scripts/insiders.py" print( f"""The moment you become a sponsor, you'll get **immediate access to {len(unreleased_features)} additional features** that you can start using right away, and which are currently exclusively available to sponsors:\n""" ) for feature in unreleased_features: feature.render(badge=True) ``` ## How to become a sponsor Thanks for your interest in sponsoring! In order to become an eligible sponsor with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. **Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, please send a short email to pawamoy@pm.me with the name of your organization and the GitHub account of the individual that should be added as a collaborator.[^4] You can cancel your sponsorship anytime.[^5] [^4]: It's currently not possible to grant access to each member of an organization, as GitHub only allows for adding users. Thus, after sponsoring, please send an email to pawamoy@pm.me, stating which account should become a collaborator of the Insiders repository. We're working on a solution which will make access to organizations much simpler. To ensure that access is not tied to a particular individual GitHub account, create a bot account (i.e. a GitHub account that is not tied to a specific individual), and use this account for the sponsoring. After being added to the list of collaborators, the bot account can create a private fork of the private Insiders GitHub repository, and grant access to all members of the organizations. [^5]: If you cancel your sponsorship, GitHub schedules a cancellation request which will become effective at the end of the billing cycle. This means that even though you cancel your sponsorship, you will keep your access to Insiders as long as your cancellation isn't effective. All charges are processed by GitHub through Stripe. As we don't receive any information regarding your payment, and GitHub doesn't offer refunds, sponsorships are non-refundable. [:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary }

If you sponsor publicly, you're automatically added here with a link to your profile and avatar to show your support for *Markdown Exec*. Alternatively, if you wish to keep your sponsorship private, you'll be a silent +1. You can select visibility during checkout and change it afterwards. ## Funding ### Goals The following section lists all funding goals. Each goal contains a list of features prefixed with a checkmark symbol, denoting whether a feature is :octicons-check-circle-fill-24:{ style="color: #00e676" } already available or :octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, but not yet implemented. When the funding goal is hit, the features are released for general availability. ```python exec="1" session="insiders" idprefix="" for goal in goals.values(): if not goal.complete: goal.render() ``` ### Goals completed This section lists all funding goals that were previously completed, which means that those features were part of Insiders, but are now generally available and can be used by all users. ```python exec="1" session="insiders" for goal in goals.values(): if goal.complete: goal.render() ``` ## Frequently asked questions ### Compatibility > We're building an open source project and want to allow outside collaborators to use *Markdown Exec* locally without having access to Insiders. Is this still possible? Yes. Insiders is compatible with *Markdown Exec*. Almost all new features and configuration options are either backward-compatible or implemented behind feature flags. Most Insiders features enhance the overall experience, though while these features add value for the users of your project, they shouldn't be necessary for previewing when making changes to content. ### Payment > We don't want to pay for sponsorship every month. Are there any other options? Yes. You can sponsor on a yearly basis by [switching your GitHub account to a yearly billing cycle][billing cycle]. If for some reason you cannot do that, you could also create a dedicated GitHub account with a yearly billing cycle, which you only use for sponsoring (some sponsors already do that). If you have any problems or further questions, please reach out to pawamoy@pm.me. ### Terms > Are we allowed to use Insiders under the same terms and conditions as *Markdown Exec*? Yes. Whether you're an individual or a company, you may use *Markdown Exec Insiders* precisely under the same terms as *Markdown Exec*, which are given by the [ISC License][license]. However, we kindly ask you to respect our **fair use policy**: - Please **don't distribute the source code** of Insiders. You may freely use it for public, private or commercial projects, privately fork or mirror it, but please don't make the source code public, as it would counteract the sponsorware strategy. - If you cancel your subscription, you're automatically removed as a collaborator and will miss out on all future updates of Insiders. However, you may **use the latest version** that's available to you **as long as you like**. Just remember that [GitHub deletes private forks][private forks]. [insiders]: #what-is-insiders [sponsorship]: #what-sponsorships-achieve [sponsors]: #how-to-become-a-sponsor [features]: #whats-in-it-for-me [funding]: #funding [goals completed]: #goals-completed [github sponsor profile]: https://github.com/sponsors/pawamoy [billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle [license]: ../license.md [private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository markdown-exec-1.8.0/docs/insiders/installation.md000066400000000000000000000155271454604644200221020ustar00rootroot00000000000000--- title: Getting started with Insiders --- # Getting started with Insiders *Markdown Exec Insiders* is a compatible drop-in replacement for *Markdown Exec*, and can be installed similarly using `pip` or `git`. Note that in order to access the Insiders repository, you need to [become an eligible sponsor] of @pawamoy on GitHub. [become an eligible sponsor]: index.md#how-to-become-a-sponsor ## Installation ### with PyPI Insiders [PyPI Insiders](https://pawamoy.github.io/pypi-insiders/) is a tool that helps you keep up-to-date versions of Insiders projects in the PyPI index of your choice (self-hosted, Google registry, Artifactory, etc.). See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). ### with pip (ssh/https) *Markdown Exec Insiders* can be installed with `pip` [using SSH][using ssh]: ```bash pip install git+ssh://git@github.com/pawamoy-insiders/markdown-exec.git ``` [using ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh Or using HTTPS: ```bash pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/markdown-exec.git ``` >? NOTE: **How to get a GitHub personal access token** > The `GH_TOKEN` environment variable is a GitHub token. > It can be obtained by creating a [personal access token] for > your GitHub account. It will give you access to the Insiders repository, > programmatically, from the command line or GitHub Actions workflows: > > 1. Go to https://github.com/settings/tokens > 2. Click on [Generate a new token] > 3. Enter a name and select the [`repo`][scopes] scope > 4. Generate the token and store it in a safe place > > [personal access token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token > [Generate a new token]: https://github.com/settings/tokens/new > [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes > > Note that the personal access > token must be kept secret at all times, as it allows the owner to access your > private repositories. ### with pip (self-hosted) Self-hosting the Insiders package makes it possible to depend on *Markdown Exec* normally, while transparently downloading and installing the Insiders version locally. It means that you can specify your dependencies normally, and your contributors without access to Insiders will get the public version, while you get the Insiders version on your machine. WARNING: **Limitation** With this method, there is no way to force the installation of an Insiders version rather than a public version. If there is a public version that is more recent than your self-hosted Insiders version, the public version will take precedence. Remember to regularly update your self-hosted versions by uploading latest distributions. You can build the distributions for Insiders yourself, by cloning the repository and using [build] to build the distributions, or you can download them from our [GitHub Releases]. You can upload these distributions to a private PyPI-like registry ([Artifactory], [Google Cloud], [pypiserver], etc.) with [Twine]: [build]: https://pypi.org/project/build/ [Artifactory]: https://jfrog.com/help/r/jfrog-artifactory-documentation/pypi-repositories [Google Cloud]: https://cloud.google.com/artifact-registry/docs/python [pypiserver]: https://pypi.org/project/pypiserver/ [Github Releases]: https://github.com/pawamoy-insiders/markdown-exec/releases [Twine]: https://pypi.org/project/twine/ ```bash # download distributions in ~/dists, then upload with: twine upload --repository-url https://your-private-index.com ~/dists/* ``` You might also need to provide a username and password/token to authenticate against the registry. Please check [Twine's documentation][twine docs]. [twine docs]: https://twine.readthedocs.io/en/stable/ You can then configure pip (or other tools) to look for packages into your package index. For example, with pip: ```bash pip config set global.extra-index-url https://your-private-index.com/simple ``` Note that the URL might differ depending on whether your are uploading a package (with Twine) or installing a package (with pip), and depending on the registry you are using (Artifactory, Google Cloud, etc.). Please check the documentation of your registry to learn how to configure your environment. **We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use](index.md#terms).** >? TIP: **Full example with `pypiserver`** > In this example we use [pypiserver] to serve a local PyPI index. > > ```bash > pip install --user pypiserver > # or pipx install pypiserver > > # create a packages directory > mkdir -p ~/.local/pypiserver/packages > > # run the pypi server without authentication > pypi-server run -p 8080 -a . -P . ~/.local/pypiserver/packages & > ``` > > We can configure the credentials to access the server in [`~/.pypirc`][pypirc]: > > [pypirc]: https://packaging.python.org/en/latest/specifications/pypirc/ > > ```ini title=".pypirc" > [distutils] > index-servers = > local > > [local] > repository: http://localhost:8080 > username: > password: > ``` > > We then clone the Insiders repository, build distributions and upload them to our local server: > > ```bash > # clone the repository > git clone git@github.com:pawamoy-insiders/markdown-exec > cd markdown-exec > > # install build > pip install --user build > # or pipx install build > > # checkout latest tag > git checkout $(git describe --tags --abbrev=0) > > # build the distributions > pyproject-build > > # upload them to our local server > twine upload -r local dist/* --skip-existing > ``` > > Finally, we configure pip, and for example [PDM][pdm], to use our local index to find packages: > > ```bash > pip config set global.extra-index-url http://localhost:8080/simple > pdm config pypi.extra.url http://localhost:8080/simple > ``` > > [pdm]: https://pdm.fming.dev/latest/ > > Now when running `pip install markdown-exec`, > or resolving dependencies with PDM, > both tools will look into our local index and find the Insiders version. > **Remember to update your local index regularly!** ### with git Of course, you can use *Markdown Exec Insiders* directly from `git`: ``` git clone git@github.com:pawamoy-insiders/markdown-exec ``` When cloning from `git`, the package must be installed: ``` pip install -e markdown-exec ``` ## Upgrading When upgrading Insiders, you should always check the version of *Markdown Exec* which makes up the first part of the version qualifier. For example, a version like `8.x.x.4.x.x` means that Insiders `4.x.x` is currently based on `8.x.x`. If the major version increased, it's a good idea to consult the [changelog] and go through the steps to ensure your configuration is up to date and all necessary changes have been made. [changelog]: ./changelog.md markdown-exec-1.8.0/docs/js/000077500000000000000000000000001454604644200156415ustar00rootroot00000000000000markdown-exec-1.8.0/docs/js/insiders.js000066400000000000000000000050621454604644200200220ustar00rootroot00000000000000function humanReadableAmount(amount) { const strAmount = String(amount); if (strAmount.length >= 4) { return `${strAmount.slice(0, strAmount.length - 3)},${strAmount.slice(-3)}`; } return strAmount; } function getJSON(url, callback) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'json'; xhr.onload = function () { var status = xhr.status; if (status === 200) { callback(null, xhr.response); } else { callback(status, xhr.response); } }; xhr.send(); } function updateInsidersPage(author_username) { const sponsorURL = `https://github.com/sponsors/${author_username}` const dataURL = `https://raw.githubusercontent.com/${author_username}/sponsors/main`; getJSON(dataURL + '/numbers.json', function (err, numbers) { document.getElementById('sponsors-count').innerHTML = numbers.count; Array.from(document.getElementsByClassName('sponsors-total')).forEach(function (element) { element.innerHTML = '$ ' + humanReadableAmount(numbers.total); }); getJSON(dataURL + '/sponsors.json', function (err, sponsors) { const sponsorsElem = document.getElementById('sponsors'); const privateSponsors = numbers.count - sponsors.length; sponsors.forEach(function (sponsor) { sponsorsElem.innerHTML += ` `; }); if (privateSponsors > 0) { sponsorsElem.innerHTML += ` +${privateSponsors} `; } }); }); getJSON(dataURL + '/sponsorsBronze.json', function (err, sponsors) { const bronzeSponsors = document.getElementById("bronze-sponsors"); if (sponsors) { let html = ''; html += 'Bronze sponsors

' sponsors.forEach(function (sponsor) { html += ` ${sponsor.name} ` }); html += '

' bronzeSponsors.innerHTML = html; } }); } markdown-exec-1.8.0/docs/license.md000066400000000000000000000000441454604644200171670ustar00rootroot00000000000000# License ``` --8<-- "LICENSE" ``` markdown-exec-1.8.0/docs/schema.json000066400000000000000000000014701454604644200173620ustar00rootroot00000000000000{ "$schema": "https://json-schema.org/draft-07/schema", "title": "Utilities to execute code blocks in Markdown files.nto your site.", "oneOf": [ { "markdownDescription": "https://pawamoy.github.io/markdown-exec", "enum": [ "markdown-exec" ] }, { "type": "object", "properties": { "markdown-exec": { "markdownDescription": "https://pawamoy.github.io/markdown-exec", "type": "object", "properties": { "languages": { "title": "The languages to enabled execution for.", "type": "array", "items": { "type": "string" } } }, "additionalProperties": false } }, "additionalProperties": false } ] }markdown-exec-1.8.0/docs/snippets/000077500000000000000000000000001454604644200170725ustar00rootroot00000000000000markdown-exec-1.8.0/docs/snippets/gallery/000077500000000000000000000000001454604644200205315ustar00rootroot00000000000000markdown-exec-1.8.0/docs/snippets/gallery/ansi.sh000066400000000000000000000010521454604644200220150ustar00rootroot00000000000000#!/bin/bash # credits to https://github.com/42picky/42picky.github.io text="xYz" # Some test text echo -e "\n 40m 41m 42m 43m 44m 45m 46m 47m" for FGs in ' m' ' 1m' ' 30m' '1;30m' ' 31m' '1;31m' ' 32m' \ '1;32m' ' 33m' '1;33m' ' 34m' '1;34m' ' 35m' '1;35m' \ ' 36m' '1;36m' ' 37m' '1;37m'; do FG=${FGs// /} echo -en " $FGs \033[$FG ${text} " for BG in 40m 41m 42m 43m 44m 45m 46m 47m; do echo -en "$EINS \033[$FG\033[${BG} ${text} \033[0m" done echo done echomarkdown-exec-1.8.0/docs/snippets/gallery/argparse.py000066400000000000000000000011321454604644200227040ustar00rootroot00000000000000import argparse from duty.cli import get_parser parser = get_parser() lines = [] lines.append(f"## duty") if parser.description: lines.append(parser.description) lines.append("\nOptions:\n") for action in parser._actions: opts = [f"`{opt}`" for opt in action.option_strings] if not opts: continue line = "- " + ",".join(opts) if action.metavar: line += f" `{action.metavar}`" line += f": {action.help}" if action.default and action.default != argparse.SUPPRESS: line += f"(default: {action.default})" lines.append(line) print("\n".join(lines)) markdown-exec-1.8.0/docs/snippets/gallery/argparse_format.py000066400000000000000000000001421454604644200242540ustar00rootroot00000000000000from duty.cli import get_parser parser = get_parser() print(f"```\n{parser.format_help()}\n```") markdown-exec-1.8.0/docs/snippets/gallery/diagrams.py000066400000000000000000000012371454604644200226750ustar00rootroot00000000000000from base64 import b64encode from contextlib import suppress from diagrams import Diagram from diagrams.k8s.clusterconfig import HPA from diagrams.k8s.compute import Deployment, Pod, ReplicaSet from diagrams.k8s.network import Ingress, Service with suppress(FileNotFoundError): with Diagram("Exposed Pod with 3 Replicas", show=False) as diagram: diagram.render = lambda: None net = Ingress("domain.com") >> Service("svc") net >> [Pod("pod1"), Pod("pod2"), Pod("pod3")] << ReplicaSet("rs") << Deployment("dp") << HPA("hpa") png = b64encode(diagram.dot.pipe(format="png")).decode() print(f'') markdown-exec-1.8.0/docs/snippets/gallery/matplotlib.py000066400000000000000000000022161454604644200232530ustar00rootroot00000000000000# https://matplotlib.org/stable/gallery/lines_bars_and_markers/scatter_demo2.html from io import StringIO import matplotlib.cbook as cbook import matplotlib.pyplot as plt import numpy as np # Load a numpy record array from yahoo csv data with fields date, open, close, # volume, adj_close from the mpl-data/example directory. The record array # stores the date as an np.datetime64 with a day unit ('D') in the date column. price_data = cbook.get_sample_data("goog.npz", np_load=True)["price_data"].view(np.recarray) price_data = price_data[-250:] # get the most recent 250 trading days delta1 = np.diff(price_data.adj_close) / price_data.adj_close[:-1] # Marker size in units of points^2 volume = (15 * price_data.volume[:-2] / price_data.volume[0]) ** 2 close = 0.003 * price_data.close[:-2] / 0.003 * price_data.open[:-2] fig, ax = plt.subplots() ax.scatter(delta1[:-1], delta1[1:], c=close, s=volume, alpha=0.5) ax.set_xlabel(r"$\Delta_i$", fontsize=15) ax.set_ylabel(r"$\Delta_{i+1}$", fontsize=15) ax.set_title("Volume and percent change") ax.grid(True) fig.tight_layout() buffer = StringIO() plt.savefig(buffer, format="svg") print(buffer.getvalue()) markdown-exec-1.8.0/docs/snippets/gallery/pydeps.py000066400000000000000000000026021454604644200224070ustar00rootroot00000000000000from pydeps import cli, colors, dot, py2depgraph from pydeps.pydeps import depgraph_to_dotsrc from pydeps.target import Target cli.verbose = cli._not_verbose options = cli.parse_args(["src/markdown_exec", "--noshow"]) colors.START_COLOR = options["start_color"] target = Target(options["fname"]) with target.chdir_work(): dep_graph = py2depgraph.py2dep(target, **options) dot_src = depgraph_to_dotsrc(target, dep_graph, **options) svg = dot.call_graphviz_dot(dot_src, "svg").decode() svg = "".join(svg.splitlines()[6:]) svg = svg.replace('fill="white"', 'fill="transparent"') reference = "../reference" modules = ( "markdown_exec", "markdown_exec.formatters", "markdown_exec.formatters.base", "markdown_exec.formatters.bash", "markdown_exec.formatters.console", "markdown_exec.formatters.markdown", "markdown_exec.formatters.pycon", "markdown_exec.formatters.python", "markdown_exec.formatters.sh", "markdown_exec.formatters.tree", "markdown_exec.logger", "markdown_exec.mkdocs_plugin", "markdown_exec.processors", "markdown_exec.rendering", ) for module in modules: svg_title = module.replace(".", "_") title_tag = f"{svg_title}" href = f"{reference}/{module.replace('.', '/')}/" svg = svg.replace(title_tag, f'{module}') svg = svg.replace("", "") print(svg) markdown-exec-1.8.0/docs/snippets/gallery/pytermgui.py000066400000000000000000000013071454604644200231310ustar00rootroot00000000000000from io import StringIO import pytermgui as ptg code = """ from contextlib import asynccontextmanager import httpx class BookClient(httpx.AsyncClient): async def get_book(self, book_id: int) -> str: response = await self.get(f"/books/{book_id}") return response.text @asynccontextmanager async def book_client(*args, **kwargs): async with BookClient(*args, **kwargs) as client: yield client """ terminal = ptg.Terminal(stream=StringIO(), size=(80, 16)) ptg.set_global_terminal(terminal) with terminal.record() as recorder: recorder.write(ptg.tim.parse(ptg.highlight_python(code))) print(recorder.export_svg(inline_styles=True)) markdown-exec-1.8.0/docs/snippets/gallery/rich.py000066400000000000000000000015471454604644200220370ustar00rootroot00000000000000import os from rich.console import Console from rich.padding import Padding from rich.syntax import Syntax code = """ from contextlib import asynccontextmanager import httpx class BookClient(httpx.AsyncClient): async def get_book(self, book_id: int) -> str: response = await self.get(f"/books/{book_id}") return response.text @asynccontextmanager async def book_client(*args, **kwargs): async with BookClient(*args, **kwargs) as client: yield client """ with open(os.devnull, "w") as devnull: console = Console(record=True, width=65, file=devnull, markup=False) renderable = Syntax(code, "python", theme="material") renderable = Padding(renderable, (0,), expand=False) console.print(renderable, markup=False) svg = console.export_svg(title="async context manager") print(svg) markdown-exec-1.8.0/docs/snippets/gallery/rich_terminal.py000066400000000000000000000061721454604644200237310ustar00rootroot00000000000000import os from rich.console import Console report = """$ griffe check griffe -ssrc -b0.24.0 -a0.23.0 [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter default was changed[/]: True -> None [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter default was changed[/]: True -> None [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]max_iterations[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/loader.py[/]:308: GriffeLoader.resolve_module_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/loader.py[/]:308: GriffeLoader.resolve_module_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:39: tmp_worktree([#7faeff]commit[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:39: tmp_worktree([#7faeff]repo[/]): [#afaf72]Positional parameter was moved[/]: position: from 2 to 1 (-1) [bold]src/griffe/git.py[/]:75: load_git([#7faeff]commit[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:75: load_git([#7faeff]repo[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]submodules[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]try_relative_path[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:75: load_git([#7faeff]extensions[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]search_paths[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]docstring_parser[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]docstring_options[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]lines_collection[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]modules_collection[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]allow_inspection[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only """ with open(os.devnull, "w") as devnull: console = Console(record=True, width=150, file=devnull) console.print(report, markup=True, highlight=False) print(console.export_html(inline_styles=True, code_format="
{code}
")) markdown-exec-1.8.0/docs/snippets/gallery/runpy.py000066400000000000000000000006761454604644200222710ustar00rootroot00000000000000import sys import warnings from contextlib import suppress from io import StringIO from runpy import run_module old_argv = list(sys.argv) sys.argv = ["mkdocs"] old_stdout = sys.stdout sys.stdout = StringIO() warnings.filterwarnings("ignore", category=RuntimeWarning) with suppress(SystemExit): run_module("mkdocs", run_name="__main__") output = sys.stdout.getvalue() sys.stdout = old_stdout sys.argv = old_argv print(f"```\n{output}\n```") markdown-exec-1.8.0/docs/snippets/gallery/textual.py000066400000000000000000000011321454604644200225660ustar00rootroot00000000000000from textual.app import App, ComposeResult from textual.widgets import Static from textual._doc import take_svg_screenshot class TextApp(App): CSS = """ Screen { background: darkblue; color: white; layout: vertical; } Static { height: auto; padding: 2; border: heavy white; background: #ffffff 30%; content-align: center middle; } """ def compose(self) -> ComposeResult: yield Static("Hello") yield Static("[b]World![/b]") print(take_svg_screenshot(app=TextApp(), terminal_size=(80, 24))) markdown-exec-1.8.0/docs/snippets/usage/000077500000000000000000000000001454604644200201765ustar00rootroot00000000000000markdown-exec-1.8.0/docs/snippets/usage/boolean_matrix.py000066400000000000000000000002441454604644200235530ustar00rootroot00000000000000print() print("a | b | a \\|\\| b") print("--- | --- | ---") for a in (True, False): for b in (True, False): print(f"{a} | {b} | {a or b}") print() markdown-exec-1.8.0/docs/snippets/usage/hide.py000066400000000000000000000000731454604644200214610ustar00rootroot00000000000000print("Hello World!") print("
") # markdown-exec: hide markdown-exec-1.8.0/docs/snippets/usage/multiple.pycon000066400000000000000000000001141454604644200230770ustar00rootroot00000000000000>>> name = "Baron" >>> print(name) Baron >>> age = "???" >>> print(age) ??? markdown-exec-1.8.0/docs/snippets/usage/platform_html.py000066400000000000000000000004361454604644200234230ustar00rootroot00000000000000import platform print( f""" """ ) markdown-exec-1.8.0/docs/snippets/usage/platform_md.py000066400000000000000000000005301454604644200230520ustar00rootroot00000000000000import platform from textwrap import dedent print( # we must dedent, otherwise Markdown # will render it as a code block! dedent( f""" - machine: `{platform.machine()}` - version: `{platform.version()}` - platform: `{platform.platform()}` - system: `{platform.system()}` """ ) ) markdown-exec-1.8.0/docs/snippets/usage/source.py000066400000000000000000000000311454604644200220420ustar00rootroot00000000000000print("I'm the result!") markdown-exec-1.8.0/docs/snippets/usage/source.pycon000066400000000000000000000000631454604644200225470ustar00rootroot00000000000000>>> print("I'm the result!") I'm not the result... markdown-exec-1.8.0/docs/usage/000077500000000000000000000000001454604644200163315ustar00rootroot00000000000000markdown-exec-1.8.0/docs/usage/index.md000066400000000000000000000316221454604644200177660ustar00rootroot00000000000000# Usage Once the extension is configured (see README/Overview), you can execute code blocks by enabling the `exec` option: ````md ```python exec="on" print("Hello Markdown!") ``` ```` The `exec` option will be true for every possible value except `0`, `no`, `off` and `false` (case insensitive). ## Options summary As the number of options grew over time, we now provide this summary listing every option, linking to their related documentation: - [`exec`](#usage): The mother of all other options, enabling code execution. - [`html`](#html-vs-markdown): Whether the output is alredady HTML, or needs to be converted from Markdown to HTML. - [`id`](#handling-errors): Give an identifier to your code blocks to help [debugging errors](#handling-errors), or to [prefix HTML ids](#html-ids). - [`idprefix`](#html-ids): Change or remove the prefix in front of HTML ids/hrefs. - [`result`](#wrap-result-in-a-code-block): Choose the syntax highlight of your code block output. - [`returncode`](shell.md#expecting-a-non-zero-exit-code): Tell what return code is expected (shell code). - [`session`](#sessions): Execute code blocks within a named session, reusing previously defined variables, etc.. - [`source`](#render-the-source-code-as-well): Render the source as well as the output. - [`tabs`](#change-the-titles-of-tabs): When rendering the source using tabs, choose the tabs titles. - [`title`](#additional-options): Title is a [Material for MkDocs][material] option. - [`updatetoc`](#generated-headings-in-table-of-contents): Whether to update the Table of Contents with generated headings. ## HTML vs. Markdown By default, Markdown Exec will render what you print as Markdown. If you want to skip rendering, to inject HTML directly, you can set the `html` option to true. HTML Example: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" System information: ```python exec="true" html="true" --8<-- "usage/platform_html.py" ``` ```` Markdown Example: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" System information: ```python exec="true" --8<-- "usage/platform_md.py" ``` ```` ## Generated headings in Table of Contents If you are using Python Markdown's `toc` extension, or writing docs with MkDocs, you will notice that the headings you generated by executing a code block appear in the table of contents. If you don't want those headings to appear in the ToC, you can use the `updatetoc="no"` boolean option: ````md ```python exec="1" updatetoc="no" print("# XL heading\n") print("## L heading\n") print("### M heading\n") print("#### S heading\n") ``` ```` ## HTML ids When your executed code blocks output Markdown, this Markdown is rendered to HTML, and every HTML id is automatically prefixed with `exec-N--`, where N is an integer incremented with each code block. To avoid breaking links, every `href` attribute is also updated when relevant. You can change this prefix, or completely remove it with the `idprefix` option. The following ids are not prefixed: ````md exec="1" source="material-block" ```python exec="1" idprefix="" updatetoc="no" print("#### Commands") print("\n[link to commands](#commands)") ``` ```` The following ids are prefixed with `cli-`: ````md exec="1" source="material-block" ```python exec="1" idprefix="cli-" updatetoc="no" print("#### Commands") print("\n[link to commands](#commands)") ``` ```` If `idprefix` is not specified, and `id` is specified, then the id is used as prefix: The following ids are prefixed with `super-cli-`: ````md exec="1" source="material-block" ```python exec="1" id="super-cli" updatetoc="no" print("#### Commands") print("\n[link to commands](#commands)") ``` ```` ## Render the source code as well It's possible to render both the result of the executed code block *and* the code block itself. For this, use the `source` option with one of the following values: - `above`: The source code will be rendered above the result. - `below`: The source code will be rendered below the result. - `material-block`: The source code and result will be wrapped in a nice-looking block (only works with [Material for MkDocs][material], and requires the [`md_in_html`][md_in_html] extension) - `tabbed-left`: The source code and result will be rendered in tabs, in that order (requires the [`pymdownx.tabbed`][pymdownx.tabbed] extension). - `tabbed-right`: The result and source code will be rendered in tabs, in that order (requires the [`pymdownx.tabbed`][pymdownx.tabbed] extension). - `console`: The source and result are concatenated in a single code block, like an interactive console session. **Source above:** ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="true" source="above" --8<-- "usage/source.py" ``` ```` --- **Source below:** ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="true" source="below" --8<-- "usage/source.py" ``` ```` --- **Material block:** ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="true" source="material-block" --8<-- "usage/source.py" ``` ```` NOTE: **Important** The `material-block` source option requires that you enable the [`md_in_html`][md_in_html] Markdown extension. --- **Tabbed on the left:** ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="true" source="tabbed-left" --8<-- "usage/source.py" ``` ```` NOTE: **Important** The `tabbed-left` source option requires that you enable the [`pymdownx.tabbed`][pymdownx.tabbed] Markdown extension. --- **Tabbed on the right:** ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="true" source="tabbed-right" --8<-- "usage/source.py" ``` ```` NOTE: **Important** The `tabbed-left` source option requires that you enable the [`pymdownx.tabbed`][pymdownx.tabbed] Markdown extension. --- **Console** (best used with actual session syntax like [`pycon`](python.md#python-console-code) or [`console`](shell.md#console)): ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```pycon exec="true" source="console" --8<-- "usage/source.pycon" ``` ```` [md_in_html]: https://python-markdown.github.io/extensions/md_in_html/ [pymdownx.tabbed]: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ ## Hiding lines from the source Every line that contains the string `markdown-exec: hide` will be hidden from the displayed source. === "Markdown" ````md ```python exec="true" source="above" --8<-- "usage/hide.py" ``` ```` === "Rendered" ```python exec="true" source="above" --8<-- "usage/hide.py" ``` ## Change the titles of tabs In the previous example, we didn't specify any title for tabs, so Markdown Exec used "Source" and "Result" by default. You can customize the titles with the `tabs` option: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="1" source="tabbed-left" tabs="Source code|Output" --8<-- "usage/source.py" ``` ```` As you can see, titles are separated with a pipe `|`. Both titles are stripped so you can add space around the pipe. If you need to use that character in a title, simply escape it with `\|`: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="1" source="tabbed-left" tabs="OR operator: a \|\| b | Boolean matrix" --8<-- "usage/boolean_matrix.py" ``` ```` IMPORTANT: The `tabs` option ***always*** expects the "Source" tab title first, and the "Result" tab title second. It allows to switch from tabbed-left to tabbed-right and inversely without having to switch the titles as well. WARNING: **Limitation** Changing the title for only one tab is not supported. ## Wrap result in a code block You can wrap the result in a code block by specifying a code block language: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```console exec="1" result="json" $ pdm info --env ``` ```` WARNING: **Limitation** Wrapping the result is not possible when HTML output is enabled. ## Additional options If you are using [Material for MkDocs][material], you are probably familiar with the `title` option on code blocks: ````md ```python title="setup.py" from setuptools import setup setup(...) ``` ```` Markdown Exec will add back these unrecognized options when rendering the source, so you can keep using them normally. Example: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="1" source="above" title="source.py" --8<-- "usage/source.py" ``` ```` ## Handling errors Code blocks execution can fail. For example, your Python code may raise exceptions, or your shell code may return a non-zero exit code (for shell commands that are expected to return non-zero, see [Expecting a non-zero exit code](shell.md#expecting-a-non-zero-exit-code)). In these cases, the exception and traceback (Python), or the current output (shell) will be rendered instead of the result, and a warning will be logged. Example of failing code: ````md ```python exec="true" print("hello") assert 1 + 1 == 11 ``` ```` ```text title="MkDocs output" WARNING - markdown_exec: Execution of python code block exited with errors ``` ```python title="Rendered traceback" Traceback (most recent call last): File "/path/to/markdown_exec/formatters/python.py", line 23, in _run_python exec(code, exec_globals) # noqa: S102 File "", line 2, in assert 1 + 1 == 11 AssertionError ``` With many executed code blocks in your docs, it will be hard to know which code block failed exactly. To make it easier, you can set an ID on each code block with the `id` option, and this ID will be shown in the logs: ````md ```python exec="true" id="print hello" print("hello") assert 1 + 1 == 11 ``` ```` ```text title="MkDocs output" WARNING - markdown_exec: Execution of python code block 'print hello' exited with errors ``` > TIP: **Titles act as IDs as well!** > You *don't need* to provide an ID > if you already set a (Material for MkDocs) title: > > ````md > ```python exec="true" title="print world" > print("world") > assert 1 + 1 == 11 > ``` > ```` > > ```text title="MkDocs output" > WARNING - markdown_exec: Execution of python code block 'print world' exited with errors > ``` ## Sessions Markdown Exec makes it possible to persist state between executed code blocks. To persist state and reuse it in other code blocks, give a session name to your blocks: ````md exec="1" source="material-block" title="Sessions" ```python exec="1" session="greet" def greet(name): print(f"Hello {name}!") ``` Hello Mushu! ```python exec="1" session="greet" greet("Ping") ``` ```` WARNING: **Limitation** Sessions only work with Python and Pycon syntax for now. ## Literate Markdown With this extension, it is also possible to write "literate programming" Markdown. From [Wikipedia](https://en.wikipedia.org/wiki/Literate_programming): > Literate programming (LP) tools are used to obtain two representations from a source file: one understandable by a compiler or interpreter, the "tangled" code, and another for viewing as formatted documentation, which is said to be "woven" from the literate source. We effectively support executing multiple *nested* code blocks to generate complex output. That makes for a very meta-markdown markup: ````md exec="1" source="tabbed-left" ```md exec="1" source="material-block" title="Markdown link" [Link to example.com](https://example.com) ``` ```` > TIP: **So power, such meta** > The above example (both tabs) was entirely generated using *a literate code block in a literate code block* 🤯: > > `````md > ````md exec="1" source="tabbed-left" > ```md exec="1" source="material-block" title="Markdown link" > [Link to example.com](https://example.com) > ``` > ```` > ````` > > In fact, all the examples on this page were generated using this method! > Check out the source here: https://github.com/pawamoy/markdown-exec/blob/master/docs/usage/index.md > (click on "Raw" to see the code blocks execution options). Of course "executing" Markdown (or rather, making it "literate") only makes sense when the source is shown as well. ## MkDocs integration As seen in the [Configuration section](../index.md#configuration), Markdown Exec can be configured directly as a MkDocs plugin: ```yaml # mkdocs.yml plugins: - search - markdown-exec ``` When configured this way, it will set a `MKDOCS_CONFIG_DIR` environment variable that you can use in your code snippets to compute file paths as relative to the MkDocs configuration file directory, instead of relative to the current working directory. This will make it possible to use the `-f` option of MkDocs, to build the documentation from a different directory than the repository root. Example: ```python exec="1" source="material-block" import os config_dir = os.environ['MKDOCS_CONFIG_DIR'] # This will show my local path since I deploy docs from my machine: print(f"Configuration file directory: `{config_dir}`") ``` The environment variable will be restored to its previous value, if any, at the end of the build. [material]: https://squidfunk.github.io/mkdocs-material/markdown-exec-1.8.0/docs/usage/pyodide.md000066400000000000000000000050371454604644200203150ustar00rootroot00000000000000# Pyodide [:octicons-tag-24: Insiders 1.0.0](../insiders/changelog.md#1.0.0) This special `pyodide` fence uses [Pyodide](https://pyodide.org), [Ace](https://ace.c9.io/) and [Highlight.js](https://highlightjs.org/) to render an interactive Python editor. Everything runs on the client side. The first time Pyodide is loaded by the browser can be a bit long, but then it will be cached and the next time you load the page it will be much faster. Click the "Run" button in the top-right corner, or hit ++ctrl+enter++ to run the code. You can install packages with Micropip: ````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" ```pyodide import micropip print("Installing cowsay...") await micropip.install("cowsay") print("done!") ``` ```` Then you can import and use the packages you installed: ````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" ```pyodide import cowsay cowsay.cow("Hello World") ``` ```` Packages installed with Micropip are cached by the browser as well, making future installations much faster. ## Sessions Editors with the same session share the same `globals()` dictionary, so you can reuse variables, classes, imports, etc., from another editor within the same session. This is why you can import `cowsay` in this editor, given you actually installed it in the first. Sessions are ephemeral: everything is reset when reloading the page. This means you cannot persist sessions across multiple pages. Try refreshing your page and running the code of the second editor: you should get a ModuleNotFoundError. To use other sessions, simply pass the `session="name"` option to the code block: ````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" ```pyodide session="something" something = "hello" ``` ```` Now lets print it in another editor with the same session: ````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" ```pyodide session="something" print(something) ``` ```` And in another editor with the default session: ````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" ```pyodide print(something) ``` ```` ## Pre-installing packages In your own documentation pages, you might not want to add `import micropip; await micropip.install("your-package")` to every editor to show how to use your package. In this case, you can use the `install` option to pre-install packages. The option takes a list of comma-separated package distribution names: ````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" ```pyodide install="griffe,dependenpy" import griffe import dependenpy print("OK!") ``` ````markdown-exec-1.8.0/docs/usage/python.md000066400000000000000000000020541454604644200201750ustar00rootroot00000000000000# Python ## Regular Python Python code is executed in the current process, with isolated global variables. To capture the output of your code, Markdown Exec patches the `print` function so that it writes to a buffer instead of standard output. ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```python exec="1" print("**Hello world!**") ``` ```` See the [Gallery](../gallery.md) for more complex examples. ## Python console code Code blocks syntax-highlighted with the `pycon` identifier are also supported. These code blocks will be pre-processed to keep only the lines starting with `>>> `, and the chevrons (prompt) will be removed from these lines, so we can execute them. ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```pycon exec="1" source="console" --8<-- "usage/source.pycon" ``` ```` It also means that multiple blocks of instructions will be concatenated, as well as their output: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```pycon exec="1" source="console" --8<-- "usage/multiple.pycon" ``` ````markdown-exec-1.8.0/docs/usage/shell.md000066400000000000000000000201401454604644200177570ustar00rootroot00000000000000# Shell Shell code blocks are executed using the same interpreter specified as language of the code block, in sub-processes. The output is captured and rendered as Markdown or HTML (see [Usage](../index.md#html-vs-markdown)). ## Bash ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```bash exec="1" source="material-block" echo $BASH_VERSION ``` ```` ## Console ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```console exec="1" source="console" $ mkdocs --help ``` ```` ## sh ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```sh exec="1" source="material-block" echo Markdown is **cool** ``` ```` ## Expecting a non-zero exit code You will sometimes want to run a command that returns a non-zero exit code, for example to show how errors look to your users. You can tell Markdown Exec to expect a particular exit code with the `returncode` option: ````md ```bash exec="true" returncode="1" echo Not in the mood today exit 1 ``` ```` In that case, the executed code won't be considered to have failed, its output will be rendered normally, and no warning will be logged in the MkDocs output, allowing your strict builds to pass. If the exit code is different than the one specified with `returncode`, it will be considered a failure, its output will be renderer anyway (stdout and stderr combined), and a warning will be logged in the MkDocs output. ## Support for ANSI colors If you installed Markdown Exec with the `ansi` extra (`pip install markdown-exec[ansi]`), the ANSI colors in the output of shell commands will be translated to HTML/CSS, allowing to render them naturally in your documentation pages. To enable ANSI colors in the output of a code block, use the [`result="ansi"` option](http://localhost:8000/markdown-exec/usage/#wrap-result-in-a-code-block). ````md exec="true" source="tabbed-left" title="ANSI terminal output" ```bash exec="true" result="ansi" --8<-- "gallery/ansi.sh" ``` ```` /// admonition type: warning Unless you enable Markdown Exec through our MkDocs plugin, you will need to provide your own CSS rules. How to do that depends on the tool(s) you use to convert Markdown to HTML, so we cannot provide generic guidance here. //// details | CSS rules used by our MkDocs plugin type: example ```css /* Inspired by https://spec.draculatheme.com/ specification, they should work decently with both dark and light themes. */ :root { --ansi-red: #ff5555; --ansi-green: #50fa7b; --ansi-blue: #265285; --ansi-yellow: #ffb86c; --ansi-magenta: #bd93f9; --ansi-cyan: #8be9fd; --ansi-black: #282a36; --ansi-white: #f8f8f2; } .-Color-Green, .-Color-Faint-Green, .-Color-Bold-Green { color: var(--ansi-green); } .-Color-Red, .-Color-Faint-Red, .-Color-Bold-Red { color: var(--ansi-red); } .-Color-Yellow, .-Color-Faint-Yellow, .-Color-Bold-Yellow { color: var(--ansi-yellow); } .-Color-Blue, .-Color-Faint-Blue, .-Color-Bold-Blue { color: var(--ansi-blue); } .-Color-Magenta, .-Color-Faint-Magenta, .-Color-Bold-Magenta { color: var(--ansi-magenta); } .-Color-Cyan, .-Color-Faint-Cyan, .-Color-Bold-Cyan { color: var(--ansi-cyan); } .-Color-White, .-Color-Faint-White, .-Color-Bold-White { color: var(--ansi-white); } .-Color-Black, .-Color-Faint-Black, .-Color-Bold-Black { color: var(--ansi-black); } .-Color-Faint { opacity: 0.5; } .-Color-Bold { font-weight: bold; } .-Color-BGBlack, .-Color-Black-BGBlack, .-Color-Blue-BGBlack, .-Color-Bold-BGBlack, .-Color-Bold-Black-BGBlack, .-Color-Bold-Green-BGBlack, .-Color-Bold-Cyan-BGBlack, .-Color-Bold-Blue-BGBlack, .-Color-Bold-Magenta-BGBlack, .-Color-Bold-Red-BGBlack, .-Color-Bold-White-BGBlack, .-Color-Bold-Yellow-BGBlack, .-Color-Cyan-BGBlack, .-Color-Green-BGBlack, .-Color-Magenta-BGBlack, .-Color-Red-BGBlack, .-Color-White-BGBlack, .-Color-Yellow-BGBlack { background-color: var(--ansi-black); } .-Color-BGRed, .-Color-Black-BGRed, .-Color-Blue-BGRed, .-Color-Bold-BGRed, .-Color-Bold-Black-BGRed, .-Color-Bold-Green-BGRed, .-Color-Bold-Cyan-BGRed, .-Color-Bold-Blue-BGRed, .-Color-Bold-Magenta-BGRed, .-Color-Bold-Red-BGRed, .-Color-Bold-White-BGRed, .-Color-Bold-Yellow-BGRed, .-Color-Cyan-BGRed, .-Color-Green-BGRed, .-Color-Magenta-BGRed, .-Color-Red-BGRed, .-Color-White-BGRed, .-Color-Yellow-BGRed { background-color: var(--ansi-red); } .-Color-BGGreen, .-Color-Black-BGGreen, .-Color-Blue-BGGreen, .-Color-Bold-BGGreen, .-Color-Bold-Black-BGGreen, .-Color-Bold-Green-BGGreen, .-Color-Bold-Cyan-BGGreen, .-Color-Bold-Blue-BGGreen, .-Color-Bold-Magenta-BGGreen, .-Color-Bold-Red-BGGreen, .-Color-Bold-White-BGGreen, .-Color-Bold-Yellow-BGGreen, .-Color-Cyan-BGGreen, .-Color-Green-BGGreen, .-Color-Magenta-BGGreen, .-Color-Red-BGGreen, .-Color-White-BGGreen, .-Color-Yellow-BGGreen { background-color: var(--ansi-green); } .-Color-BGYellow, .-Color-Black-BGYellow, .-Color-Blue-BGYellow, .-Color-Bold-BGYellow, .-Color-Bold-Black-BGYellow, .-Color-Bold-Green-BGYellow, .-Color-Bold-Cyan-BGYellow, .-Color-Bold-Blue-BGYellow, .-Color-Bold-Magenta-BGYellow, .-Color-Bold-Red-BGYellow, .-Color-Bold-White-BGYellow, .-Color-Bold-Yellow-BGYellow, .-Color-Cyan-BGYellow, .-Color-Green-BGYellow, .-Color-Magenta-BGYellow, .-Color-Red-BGYellow, .-Color-White-BGYellow, .-Color-Yellow-BGYellow { background-color: var(--ansi-yellow); } .-Color-BGBlue, .-Color-Black-BGBlue, .-Color-Blue-BGBlue, .-Color-Bold-BGBlue, .-Color-Bold-Black-BGBlue, .-Color-Bold-Green-BGBlue, .-Color-Bold-Cyan-BGBlue, .-Color-Bold-Blue-BGBlue, .-Color-Bold-Magenta-BGBlue, .-Color-Bold-Red-BGBlue, .-Color-Bold-White-BGBlue, .-Color-Bold-Yellow-BGBlue, .-Color-Cyan-BGBlue, .-Color-Green-BGBlue, .-Color-Magenta-BGBlue, .-Color-Red-BGBlue, .-Color-White-BGBlue, .-Color-Yellow-BGBlue { background-color: var(--ansi-blue); } .-Color-BGMagenta, .-Color-Black-BGMagenta, .-Color-Blue-BGMagenta, .-Color-Bold-BGMagenta, .-Color-Bold-Black-BGMagenta, .-Color-Bold-Green-BGMagenta, .-Color-Bold-Cyan-BGMagenta, .-Color-Bold-Blue-BGMagenta, .-Color-Bold-Magenta-BGMagenta, .-Color-Bold-Red-BGMagenta, .-Color-Bold-White-BGMagenta, .-Color-Bold-Yellow-BGMagenta, .-Color-Cyan-BGMagenta, .-Color-Green-BGMagenta, .-Color-Magenta-BGMagenta, .-Color-Red-BGMagenta, .-Color-White-BGMagenta, .-Color-Yellow-BGMagenta { background-color: var(--ansi-magenta); } .-Color-BGCyan, .-Color-Black-BGCyan, .-Color-Blue-BGCyan, .-Color-Bold-BGCyan, .-Color-Bold-Black-BGCyan, .-Color-Bold-Green-BGCyan, .-Color-Bold-Cyan-BGCyan, .-Color-Bold-Blue-BGCyan, .-Color-Bold-Magenta-BGCyan, .-Color-Bold-Red-BGCyan, .-Color-Bold-White-BGCyan, .-Color-Bold-Yellow-BGCyan, .-Color-Cyan-BGCyan, .-Color-Green-BGCyan, .-Color-Magenta-BGCyan, .-Color-Red-BGCyan, .-Color-White-BGCyan, .-Color-Yellow-BGCyan { background-color: var(--ansi-cyan); } .-Color-BGWhite, .-Color-Black-BGWhite, .-Color-Blue-BGWhite, .-Color-Bold-BGWhite, .-Color-Bold-Black-BGWhite, .-Color-Bold-Green-BGWhite, .-Color-Bold-Cyan-BGWhite, .-Color-Bold-Blue-BGWhite, .-Color-Bold-Magenta-BGWhite, .-Color-Bold-Red-BGWhite, .-Color-Bold-White-BGWhite, .-Color-Bold-Yellow-BGWhite, .-Color-Cyan-BGWhite, .-Color-Green-BGWhite, .-Color-Magenta-BGWhite, .-Color-Red-BGWhite, .-Color-White-BGWhite, .-Color-Yellow-BGWhite { background-color: var(--ansi-white); } .-Color-Black, .-Color-Bold-Black, .-Color-Black-BGBlack, .-Color-Bold-Black-BGBlack, .-Color-Black-BGGreen, .-Color-Red-BGRed, .-Color-Bold-Red-BGRed, .-Color-Bold-Blue-BGBlue, .-Color-Blue-BGBlue { text-shadow: 0 0 1px var(--ansi-white); } .-Color-Bold-Cyan-BGCyan, .-Color-Bold-Magenta-BGMagenta, .-Color-Bold-White, .-Color-Bold-Yellow-BGYellow, .-Color-Bold-Green-BGGreen, .-Color-Cyan-BGCyan, .-Color-Cyan-BGGreen, .-Color-Green-BGCyan, .-Color-Green-BGGreen, .-Color-Magenta-BGMagenta, .-Color-White, .-Color-White-BGWhite, .-Color-Yellow-BGYellow { text-shadow: 0 0 1px var(--ansi-black); } ``` //// /// > IMPORTANT: We also recommend setting `ansi: required` in `mkdocs.yml` > when using our MkDocs plugin and enabling ANSI support, > to help tools like MkDocs and its `get-deps` command > know that the `ansi` extra dependency is required. > > ```yaml > plugins: > - markdown-exec: > ansi: required > ``` markdown-exec-1.8.0/docs/usage/tree.md000066400000000000000000000025651454604644200176220ustar00rootroot00000000000000# Tree Markdown Exec provides a `tree` formatter that can be used to render file-system trees easily: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```tree root1 file1 dir1 file dir2 file1 file2 file2 file3 root2 file1 ``` ```` By default, the language used for syntax highlight is `bash`. It means you can add comments with `#`: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```tree root1 # comment 1 file1 dir1 file dir2 file1 # comment 2 file2 # comment 3 file2 file3 root2 file1 ``` ```` You can change the syntax highlight language with the `result` option: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```tree result="javascript" root1 // comment 1 file1 dir1 file dir2 file1 // comment 2 file2 // comment 3 file2 file3 root2 file1 ``` ```` You can force an entry to be displayed as a directory instead of a regular file by appending a trailing slash to the name: ````md exec="1" source="tabbed-left" tabs="Markdown|Rendered" ```tree root1 dir1/ dir2/ dir3/ ``` ```` It is recommended to always append trailing slashes to directory anyway. WARNING: **Limitation** Spaces in file names are not supported when searching for a trailing slash. markdown-exec-1.8.0/duties.py000066400000000000000000000225621454604644200161530ustar00rootroot00000000000000"""Development tasks.""" from __future__ import annotations import os import sys from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING, Iterator from duty import duty from duty.callables import black, coverage, lazy, mkdocs, mypy, pytest, ruff, safety if TYPE_CHECKING: from duty.context import Context PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) PY_SRC = " ".join(PY_SRC_LIST) CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} WINDOWS = os.name == "nt" PTY = not WINDOWS and not CI MULTIRUN = os.environ.get("PDM_MULTIRUN", "0") == "1" def pyprefix(title: str) -> str: # noqa: D103 if MULTIRUN: prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" return f"{prefix:14}{title}" return title @contextmanager def material_insiders() -> Iterator[bool]: # noqa: D103 if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: yield True finally: os.environ.pop("MATERIAL_INSIDERS") else: yield False below_312 = sys.version_info < (3, 12) skip_docs_reason = pyprefix("Building docs is not supported on Python 3.12, skipping") @duty def changelog(ctx: Context) -> None: """Update the changelog in-place with latest commits. Parameters: ctx: The context instance (passed automatically). """ from git_changelog.cli import main as git_changelog ctx.run(git_changelog, args=[[]], title="Updating changelog") @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) def check(ctx: Context) -> None: # noqa: ARG001 """Check it all! Parameters: ctx: The context instance (passed automatically). """ @duty def check_quality(ctx: Context) -> None: """Check the code quality. Parameters: ctx: The context instance (passed automatically). """ ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), command=f"ruff check --config config/ruff.toml {PY_SRC}", ) @duty def check_dependencies(ctx: Context) -> None: """Check for vulnerabilities in dependencies. Parameters: ctx: The context instance (passed automatically). """ # retrieve the list of dependencies requirements = ctx.run( ["pdm", "export", "-f", "requirements", "--without-hashes"], title="Exporting dependencies as requirements", allow_overrides=False, ) ctx.run( safety.check(requirements), title="Checking dependencies", command="pdm export -f requirements --without-hashes | safety check --stdin", ) @duty(skip_if=not below_312, skip_reason=skip_docs_reason) def check_docs(ctx: Context) -> None: """Check if the documentation builds correctly. Parameters: ctx: The context instance (passed automatically). """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) with material_insiders(): ctx.run( mkdocs.build(strict=True, verbose=True), title=pyprefix("Building documentation"), command="mkdocs build -vs", ) @duty def check_types(ctx: Context) -> None: """Check that the code is correctly typed. Parameters: ctx: The context instance (passed automatically). """ ctx.run( mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @duty def check_api(ctx: Context) -> None: """Check for API breaking changes. Parameters: ctx: The context instance (passed automatically). """ from griffe.cli import check as g_check griffe_check = lazy(g_check, name="griffe.check") ctx.run( griffe_check("markdown_exec", search_paths=["src"], color=True), title="Checking for API breaking changes", command="griffe check -ssrc markdown_exec", nofail=True, ) @duty(silent=True) def clean(ctx: Context) -> None: """Delete temporary files. Parameters: ctx: The context instance (passed automatically). """ ctx.run("rm -rf .coverage*") ctx.run("rm -rf .mypy_cache") ctx.run("rm -rf .pytest_cache") ctx.run("rm -rf tests/.pytest_cache") ctx.run("rm -rf build") ctx.run("rm -rf dist") ctx.run("rm -rf htmlcov") ctx.run("rm -rf pip-wheel-metadata") ctx.run("rm -rf site") ctx.run("find . -type d -name __pycache__ | xargs rm -rf") ctx.run("find . -name '*.rej' -delete") @duty(skip_if=not below_312, skip_reason=skip_docs_reason) def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). Parameters: ctx: The context instance (passed automatically). host: The host to serve the docs from. port: The port to serve the docs on. """ with material_insiders(): ctx.run( mkdocs.serve(dev_addr=f"{host}:{port}"), title="Serving documentation", capture=False, ) @duty(skip_if=not below_312, skip_reason=skip_docs_reason) def docs_deploy(ctx: Context) -> None: """Deploy the documentation on GitHub pages. Parameters: ctx: The context instance (passed automatically). """ os.environ["DEPLOY"] = "true" with material_insiders() as insiders: if not insiders: ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") origin = ctx.run("git config --get remote.origin.url", silent=True) if "pawamoy-insiders/markdown-exec" in origin: ctx.run("git remote add upstream git@github.com:pawamoy/markdown-exec", silent=True, nofail=True) ctx.run( mkdocs.gh_deploy(remote_name="upstream", force=True), title="Deploying documentation", ) else: ctx.run( lambda: False, title="Not deploying docs from public repository (do that from insiders instead!)", nofail=True, ) @duty def format(ctx: Context) -> None: """Run formatting tools on the code. Parameters: ctx: The context instance (passed automatically). """ ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) ctx.run(black.run(*PY_SRC_LIST, config="config/black.toml"), title="Formatting code") @duty(post=["docs-deploy"]) def release(ctx: Context, version: str) -> None: """Release a new Python package. Parameters: ctx: The context instance (passed automatically). version: The new version number to use. """ origin = ctx.run("git config --get remote.origin.url", silent=True) if "pawamoy-insiders/markdown-exec" in origin: ctx.run( lambda: False, title="Not releasing from insiders repository (do that from public repo instead!)", ) ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY) ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) ctx.run("git push", title="Pushing commits", pty=False) ctx.run("git push --tags", title="Pushing tags", pty=False) ctx.run("pdm build", title="Building dist/wheel", pty=PTY) ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) @duty(silent=True, aliases=["coverage"]) def cov(ctx: Context) -> None: """Report coverage as text and HTML. Parameters: ctx: The context instance (passed automatically). """ ctx.run(coverage.combine, nofail=True) ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) ctx.run(coverage.html(rcfile="config/coverage.ini")) @duty def test(ctx: Context, match: str = "") -> None: """Run the test suite. Parameters: ctx: The context instance (passed automatically). match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes"), title=pyprefix("Running tests"), command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) @duty def vscode(ctx: Context) -> None: """Configure VSCode. This task will overwrite the following files, so make sure to back them up: - `.vscode/launch.json` - `.vscode/settings.json` - `.vscode/tasks.json` Parameters: ctx: The context instance (passed automatically). """ def update_config(filename: str) -> None: source_file = Path("config", "vscode", filename) target_file = Path(".vscode", filename) target_file.parent.mkdir(exist_ok=True) target_file.write_text(source_file.read_text()) for filename in ("launch.json", "settings.json", "tasks.json"): ctx.run(update_config, args=[filename], title=f"Update .vscode/{filename}") markdown-exec-1.8.0/mkdocs.yml000066400000000000000000000110041454604644200162740ustar00rootroot00000000000000site_name: "Markdown Exec" site_description: "Utilities to execute code blocks in Markdown files." site_url: "https://pawamoy.github.io/markdown-exec" repo_url: "https://github.com/pawamoy/markdown-exec" repo_name: "pawamoy/markdown-exec" site_dir: "site" watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/markdown_exec] copyright: Copyright © 2022 Timothée Mazzucotelli edit_uri: edit/main/docs/ validation: omitted_files: warn absolute_links: warn unrecognized_links: warn nav: - Home: - Overview: index.md - Changelog: changelog.md - Credits: credits.md - License: license.md - Usage: - usage/index.md - Python: usage/python.md - Pyodide: usage/pyodide.md - Shell: usage/shell.md - Tree: usage/tree.md - Gallery: gallery.md # defer to gen-files + literate-nav - API reference: - Markdown Exec: reference/ - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md - Coverage report: coverage.md - Insiders: - insiders/index.md - Getting started: - Installation: insiders/installation.md - Changelog: insiders/changelog.md - Author's website: https://pawamoy.github.io/ theme: name: material custom_dir: docs/.overrides icon: logo: material/currency-sign features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.footer - navigation.indexes - navigation.sections - navigation.tabs - navigation.tabs.sticky - navigation.top - search.highlight - search.suggest - toc.follow palette: - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default primary: teal accent: purple toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: black accent: lime toggle: icon: material/weather-night name: Switch to system preference extra_css: - css/material.css - css/mkdocstrings.css - css/insiders.css markdown_extensions: - admonition - attr_list - callouts: strip_period: false - footnotes - md_in_html - pymdownx.blocks.admonition - pymdownx.blocks.details - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.magiclink - pymdownx.snippets: base_path: [!relative $config_dir] check_paths: true base_path: [docs/snippets, "."] - pymdownx.highlight - pymdownx.keys - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower - pymdownx.tasklist: custom_checkbox: true - toc: permalink: "¤" plugins: - search - markdown-exec: ansi: required - gen-files: scripts: - scripts/gen_ref_nav.py - literate-nav: nav_file: SUMMARY.md - coverage - mkdocstrings: handlers: python: import: - https://docs.python.org/3/objects.inv - https://python-markdown.github.io/objects.inv - https://www.mkdocs.org/objects.inv paths: [src] options: docstring_options: ignore_init_summary: true docstring_section_style: list filters: ["!^_"] heading_level: 1 inherited_members: true merge_init_into_class: true separate_signature: true show_root_heading: true show_root_full_path: false show_signature_annotations: true show_source: false show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true - git-committers: enabled: !ENV [DEPLOY, false] repository: pawamoy/markdown-exec - minify: minify_html: !ENV [DEPLOY, false] - group: enabled: !ENV [MATERIAL_INSIDERS, false] plugins: - typeset extra: social: - icon: fontawesome/brands/github link: https://github.com/pawamoy - icon: fontawesome/brands/mastodon link: https://fosstodon.org/@pawamoy - icon: fontawesome/brands/twitter link: https://twitter.com/pawamoy - icon: fontawesome/brands/gitter link: https://gitter.im/markdown-exec/community - icon: fontawesome/brands/python link: https://pypi.org/project/markdown-exec/ markdown-exec-1.8.0/pyproject.toml000066400000000000000000000060551454604644200172170ustar00rootroot00000000000000[build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [project] name = "markdown-exec" description = "Utilities to execute code blocks in Markdown files." authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}] license = {text = "ISC"} readme = "README.md" requires-python = ">=3.8" keywords = ["markdown", "python", "exec", "shell", "bash", "mkdocs"] dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Utilities", "Typing :: Typed", ] dependencies = [ "pymdown-extensions>=9", ] [project.optional-dependencies] ansi = ["pygments-ansi-color"] [project.urls] Homepage = "https://pawamoy.github.io/markdown-exec" Documentation = "https://pawamoy.github.io/markdown-exec" Changelog = "https://pawamoy.github.io/markdown-exec/changelog" Repository = "https://github.com/pawamoy/markdown-exec" Issues = "https://github.com/pawamoy/markdown-exec/issues" Discussions = "https://github.com/pawamoy/markdown-exec/discussions" Gitter = "https://gitter.im/markdown-exec/community" Funding = "https://github.com/sponsors/pawamoy" [project.entry-points."mkdocs.plugins"] markdown-exec = "markdown_exec.mkdocs_plugin:MarkdownExecPlugin" [tool.pdm] version = {source = "scm"} plugins = [ "pdm-multirun", ] [tool.pdm.build] package-dir = "src" editable-backend = "editables" [tool.pdm.dev-dependencies] duty = ["duty>=0.10"] ci-quality = ["markdown-exec[duty,docs,quality,typing,security]"] ci-tests = ["markdown-exec[duty,tests]"] docs = [ "black>=23.9", "markdown-callouts>=0.3", "mkdocs>=1.5", "mkdocs-coverage>=1.0", "mkdocs-gen-files>=0.5", "mkdocs-git-committers-plugin-2>=1.2", "mkdocs-literate-nav>=0.6", "mkdocs-material>=9.4", "mkdocs-minify-plugin>=0.7", "mkdocstrings[python]>=0.23", "tomli>=2.0; python_version < '3.11'", # gallery dependencies "toml>=0.10; python_version < '3.12'", "pydeps>=1.12; python_version < '3.12'", "diagrams>=0.21; python_version < '3.12'", "rich>=12.3; python_version < '3.12'", "matplotlib>=3.5; python_version < '3.12'", "numpy>=1.23; python_version < '3.12'", "textual; python_version < '3.12'", "pytermgui>=6.3; python_version < '3.12'", "pipdeptree>=2.6; python_version < '3.12'", ] maintain = [ "black>=23.9", "blacken-docs>=1.16", "git-changelog>=2.3", ] quality = [ "ruff>=0.0", ] tests = [ "pytest>=7.4", "pytest-cov>=4.1", "pytest-randomly>=3.15", "pytest-xdist>=3.3", "pygments>=2.15", ] typing = [ "mypy>=1.5", "types-markdown>=3.5", "types-pyyaml>=6.0", ] security = [ "safety>=2.3", ] markdown-exec-1.8.0/scripts/000077500000000000000000000000001454604644200157645ustar00rootroot00000000000000markdown-exec-1.8.0/scripts/gen_credits.py000066400000000000000000000114701454604644200206270ustar00rootroot00000000000000"""Script to generate the project's credits.""" from __future__ import annotations import os import re import sys from importlib.metadata import PackageNotFoundError, metadata from itertools import chain from pathlib import Path from textwrap import dedent from typing import Mapping, cast from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment # TODO: Remove once support for Python 3.10 is dropped. if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", ".")) with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: pyproject = tomllib.load(pyproject_file) project = pyproject["project"] pdm = pyproject["tool"]["pdm"] with project_dir.joinpath("pdm.lock").open("rb") as lock_file: lock_data = tomllib.load(lock_file) lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} project_name = project["name"] regex = re.compile(r"(?P[\w.-]+)(?P.*)$") def _get_license(pkg_name: str) -> str: try: data = metadata(pkg_name) except PackageNotFoundError: return "?" license_name = cast(dict, data).get("License", "").strip() multiple_lines = bool(license_name.count("\n")) # TODO: Remove author logic once all my packages licenses are fixed. author = "" if multiple_lines or not license_name or license_name == "UNKNOWN": for header, value in cast(dict, data).items(): if header == "Classifier" and value.startswith("License ::"): license_name = value.rsplit("::", 1)[1].strip() elif header == "Author-email": author = value if license_name == "Other/Proprietary License" and "pawamoy" in author: license_name = "ISC" return license_name or "?" def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str, str]]: deps = {} for dep in base_deps: parsed = regex.match(dep).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() if dep_name not in lock_pkgs: continue deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True while again: again = False for pkg_name in lock_pkgs: if pkg_name in deps: for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() if dep_name in lock_pkgs and dep_name not in deps and dep_name != project["name"]: deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True return deps def _render_credits() -> str: dev_dependencies = _get_deps(chain(*pdm.get("dev-dependencies", {}).values())) # type: ignore[arg-type] prod_dependencies = _get_deps( chain( # type: ignore[arg-type] project.get("dependencies", []), chain(*project.get("optional-dependencies", {}).values()), ), ) template_data = { "project_name": project_name, "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), "more_credits": "http://pawamoy.github.io/credits/", } template_text = dedent( """ # Credits These projects were used to build *{{ project_name }}*. **Thank you!** [`python`](https://www.python.org/) | [`pdm`](https://pdm.fming.dev/) | [`copier-pdm`](https://github.com/pawamoy/copier-pdm) {% macro dep_line(dep) -%} [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in prod_dependencies -%} {{ dep_line(dep) }} {% endfor %} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in dev_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} """, ) jinja_env = SandboxedEnvironment(undefined=StrictUndefined) return jinja_env.from_string(template_text).render(**template_data) print(_render_credits()) markdown-exec-1.8.0/scripts/gen_ref_nav.py000066400000000000000000000021631454604644200206110ustar00rootroot00000000000000"""Generate the code reference pages and navigation.""" from pathlib import Path import mkdocs_gen_files nav = mkdocs_gen_files.Nav() mod_symbol = '' src = Path(__file__).parent.parent / "src" for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") elif parts[-1].startswith("_"): continue nav_parts = [f"{mod_symbol} {part}" for part in parts] nav[tuple(nav_parts)] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) fd.write(f"::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) markdown-exec-1.8.0/scripts/insiders.py000066400000000000000000000145731454604644200201700ustar00rootroot00000000000000"""Functions related to Insiders funding goals.""" from __future__ import annotations import json import logging import os import posixpath from dataclasses import dataclass from datetime import date, datetime, timedelta from itertools import chain from pathlib import Path from typing import Iterable, cast from urllib.error import HTTPError from urllib.parse import urljoin from urllib.request import urlopen import yaml logger = logging.getLogger(f"mkdocs.logs.{__name__}") def human_readable_amount(amount: int) -> str: # noqa: D103 str_amount = str(amount) if len(str_amount) >= 4: # noqa: PLR2004 return f"{str_amount[:len(str_amount)-3]},{str_amount[-3:]}" return str_amount @dataclass class Project: """Class representing an Insiders project.""" name: str url: str @dataclass class Feature: """Class representing an Insiders feature.""" name: str ref: str | None since: date | None project: Project | None def url(self, rel_base: str = "..") -> str | None: # noqa: D102 if not self.ref: return None if self.project: rel_base = self.project.url return posixpath.join(rel_base, self.ref.lstrip("/")) def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: D102 new = "" if badge: recent = self.since and date.today() - self.since <= timedelta(days=60) # noqa: DTZ011 if recent: ft_date = self.since.strftime("%B %d, %Y") # type: ignore[union-attr] new = f' :material-alert-decagram:{{ .new-feature .vibrate title="Added on {ft_date}" }}' project = f"[{self.project.name}]({self.project.url}) — " if self.project else "" feature = f"[{self.name}]({self.url(rel_base)})" if self.ref else self.name print(f"- [{'x' if self.since else ' '}] {project}{feature}{new}") @dataclass class Goal: """Class representing an Insiders goal.""" name: str amount: int features: list[Feature] complete: bool = False @property def human_readable_amount(self) -> str: # noqa: D102 return human_readable_amount(self.amount) def render(self, rel_base: str = "..") -> None: # noqa: D102 print(f"#### $ {self.human_readable_amount} — {self.name}\n") for feature in self.features: feature.render(rel_base) print("") def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]: """Load goals from JSON data. Parameters: data: The JSON data. funding: The current total funding, per month. origin: The origin of the data (URL). Returns: A dictionaries of goals, keys being their target monthly amount. """ goals_data = yaml.safe_load(data)["goals"] return { amount: Goal( name=goal_data["name"], amount=amount, complete=funding >= amount, features=[ Feature( name=feature_data["name"], ref=feature_data.get("ref"), since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 project=project, ) for feature_data in goal_data["features"] ], ) for amount, goal_data in goals_data.items() } def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]: project_dir = os.getenv("MKDOCS_CONFIG_DIR", ".") try: data = Path(project_dir, path).read_text() except OSError as error: raise RuntimeError(f"Could not load data from disk: {path}") from error return load_goals(data, funding) def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: project_name, project_url, data_fragment = source_data data_url = urljoin(project_url, data_fragment) try: with urlopen(data_url) as response: # noqa: S310 data = response.read() except HTTPError as error: raise RuntimeError(f"Could not load data from network: {data_url}") from error return load_goals(data, funding, project=Project(name=project_name, url=project_url)) def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: if isinstance(source, str): return _load_goals_from_disk(source, funding) return _load_goals_from_url(source, funding) def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]: """Load funding goals from a given data source. Parameters: source: The data source (local file path or URL). funding: The current total funding, per month. Returns: A dictionaries of goals, keys being their target monthly amount. """ if isinstance(source, str): return _load_goals_from_disk(source, funding) goals = {} for src in source: source_goals = _load_goals(src) for amount, goal in source_goals.items(): if amount not in goals: goals[amount] = goal else: goals[amount].features.extend(goal.features) return {amount: goals[amount] for amount in sorted(goals)} def feature_list(goals: Iterable[Goal]) -> list[Feature]: """Extract feature list from funding goals. Parameters: goals: A list of funding goals. Returns: A list of features. """ return list(chain.from_iterable(goal.features for goal in goals)) def load_json(url: str) -> str | list | dict: # noqa: D103 with urlopen(url) as response: # noqa: S310 return json.loads(response.read().decode()) data_source = globals()["data_source"] sponsor_url = "https://github.com/sponsors/pawamoy" data_url = "https://raw.githubusercontent.com/pawamoy/sponsors/main" numbers: dict[str, int] = load_json(f"{data_url}/numbers.json") # type: ignore[assignment] sponsors: list[dict] = load_json(f"{data_url}/sponsors.json") # type: ignore[assignment] current_funding = numbers["total"] sponsors_count = numbers["count"] goals = funding_goals(data_source, funding=current_funding) ongoing_goals = [goal for goal in goals.values() if not goal.complete] unreleased_features = sorted( (ft for ft in feature_list(ongoing_goals) if ft.since), key=lambda ft: cast(date, ft.since), reverse=True, ) markdown-exec-1.8.0/scripts/setup.sh000077500000000000000000000006021454604644200174610ustar00rootroot00000000000000#!/usr/bin/env bash set -e if ! command -v pdm &>/dev/null; then if ! command -v pipx &>/dev/null; then python3 -m pip install --user pipx fi pipx install pdm fi if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then pdm install --plugins fi if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then pdm multirun -v pdm install -G:all else pdm install -G:all fi markdown-exec-1.8.0/src/000077500000000000000000000000001454604644200150645ustar00rootroot00000000000000markdown-exec-1.8.0/src/markdown_exec/000077500000000000000000000000001454604644200177125ustar00rootroot00000000000000markdown-exec-1.8.0/src/markdown_exec/__init__.py000066400000000000000000000104161454604644200220250ustar00rootroot00000000000000"""Markdown Exec package. Utilities to execute code blocks in Markdown files. """ # https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#snippets from __future__ import annotations import re from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from markdown import Markdown from markdown_exec.formatters.base import default_tabs from markdown_exec.formatters.bash import _format_bash from markdown_exec.formatters.console import _format_console from markdown_exec.formatters.markdown import _format_markdown from markdown_exec.formatters.pycon import _format_pycon from markdown_exec.formatters.pyodide import _format_pyodide from markdown_exec.formatters.python import _format_python from markdown_exec.formatters.sh import _format_sh from markdown_exec.formatters.tree import _format_tree __all__: list[str] = ["formatter", "validator"] formatters = { "bash": _format_bash, "console": _format_console, "md": _format_markdown, "markdown": _format_markdown, "py": _format_python, "python": _format_python, "pycon": _format_pycon, "pyodide": _format_pyodide, "sh": _format_sh, "tree": _format_tree, } # negative look behind: matches only if | (pipe) if not preceded by \ (backslash) _tabs_re = re.compile(r"(? bool: """Validate code blocks inputs. Parameters: language: The code language, like python or bash. inputs: The code block inputs, to be sorted into options and attrs. options: The container for options. attrs: The container for attrs: md: The Markdown instance. Returns: Success or not. """ exec_value = _to_bool(inputs.pop("exec", "no")) if language not in {"tree", "pyodide"} and not exec_value: return False id_value = inputs.pop("id", "") id_prefix_value = inputs.pop("idprefix", None) html_value = _to_bool(inputs.pop("html", "no")) source_value = inputs.pop("source", "") result_value = inputs.pop("result", "") returncode_value = int(inputs.pop("returncode", "0")) session_value = inputs.pop("session", "") update_toc_value = _to_bool(inputs.pop("updatetoc", "yes")) tabs_value = inputs.pop("tabs", "|".join(default_tabs)) tabs = tuple(_tabs_re.split(tabs_value, maxsplit=1)) options["id"] = id_value options["id_prefix"] = id_prefix_value options["html"] = html_value options["source"] = source_value options["result"] = result_value options["returncode"] = returncode_value options["session"] = session_value options["update_toc"] = update_toc_value options["tabs"] = tabs options["extra"] = inputs return True def formatter( source: str, language: str, css_class: str, # noqa: ARG001 options: dict[str, Any], md: Markdown, classes: list[str] | None = None, # noqa: ARG001 id_value: str = "", # noqa: ARG001 attrs: dict[str, Any] | None = None, # noqa: ARG001 **kwargs: Any, # noqa: ARG001 ) -> str: """Execute code and return HTML. Parameters: source: The code to execute. language: The code language, like python or bash. css_class: The CSS class to add to the HTML element. options: The container for options. attrs: The container for attrs: md: The Markdown instance. classes: Additional CSS classes. id_value: An optional HTML id. attrs: Additional attributes **kwargs: Additional arguments passed to SuperFences default formatters. Returns: HTML contents. """ fmt = formatters.get(language, lambda source, **kwargs: source) return fmt(code=source, md=md, **options) # type: ignore[operator] falsy_values = {"", "no", "off", "false", "0"} truthy_values = {"yes", "on", "true", "1"} def _to_bool(value: str) -> bool: return value.lower() not in falsy_values def _to_bool_or_value(value: str) -> bool | str: if value.lower() in falsy_values: return False if value.lower() in truthy_values: return True return value markdown-exec-1.8.0/src/markdown_exec/ansi.css000066400000000000000000000130311454604644200213540ustar00rootroot00000000000000/* Inspired by https://spec.draculatheme.com/ specification, they should work decently with both dark and light themes. */ :root { --ansi-red: #ff5555; --ansi-green: #50fa7b; --ansi-blue: #265285; --ansi-yellow: #ffb86c; --ansi-magenta: #bd93f9; --ansi-cyan: #8be9fd; --ansi-black: #282a36; --ansi-white: #f8f8f2; } .-Color-Green, .-Color-Faint-Green, .-Color-Bold-Green { color: var(--ansi-green); } .-Color-Red, .-Color-Faint-Red, .-Color-Bold-Red { color: var(--ansi-red); } .-Color-Yellow, .-Color-Faint-Yellow, .-Color-Bold-Yellow { color: var(--ansi-yellow); } .-Color-Blue, .-Color-Faint-Blue, .-Color-Bold-Blue { color: var(--ansi-blue); } .-Color-Magenta, .-Color-Faint-Magenta, .-Color-Bold-Magenta { color: var(--ansi-magenta); } .-Color-Cyan, .-Color-Faint-Cyan, .-Color-Bold-Cyan { color: var(--ansi-cyan); } .-Color-White, .-Color-Faint-White, .-Color-Bold-White { color: var(--ansi-white); } .-Color-Black, .-Color-Faint-Black, .-Color-Bold-Black { color: var(--ansi-black); } .-Color-Faint { opacity: 0.5; } .-Color-Bold { font-weight: bold; } .-Color-BGBlack, .-Color-Black-BGBlack, .-Color-Blue-BGBlack, .-Color-Bold-BGBlack, .-Color-Bold-Black-BGBlack, .-Color-Bold-Green-BGBlack, .-Color-Bold-Cyan-BGBlack, .-Color-Bold-Blue-BGBlack, .-Color-Bold-Magenta-BGBlack, .-Color-Bold-Red-BGBlack, .-Color-Bold-White-BGBlack, .-Color-Bold-Yellow-BGBlack, .-Color-Cyan-BGBlack, .-Color-Green-BGBlack, .-Color-Magenta-BGBlack, .-Color-Red-BGBlack, .-Color-White-BGBlack, .-Color-Yellow-BGBlack { background-color: var(--ansi-black); } .-Color-BGRed, .-Color-Black-BGRed, .-Color-Blue-BGRed, .-Color-Bold-BGRed, .-Color-Bold-Black-BGRed, .-Color-Bold-Green-BGRed, .-Color-Bold-Cyan-BGRed, .-Color-Bold-Blue-BGRed, .-Color-Bold-Magenta-BGRed, .-Color-Bold-Red-BGRed, .-Color-Bold-White-BGRed, .-Color-Bold-Yellow-BGRed, .-Color-Cyan-BGRed, .-Color-Green-BGRed, .-Color-Magenta-BGRed, .-Color-Red-BGRed, .-Color-White-BGRed, .-Color-Yellow-BGRed { background-color: var(--ansi-red); } .-Color-BGGreen, .-Color-Black-BGGreen, .-Color-Blue-BGGreen, .-Color-Bold-BGGreen, .-Color-Bold-Black-BGGreen, .-Color-Bold-Green-BGGreen, .-Color-Bold-Cyan-BGGreen, .-Color-Bold-Blue-BGGreen, .-Color-Bold-Magenta-BGGreen, .-Color-Bold-Red-BGGreen, .-Color-Bold-White-BGGreen, .-Color-Bold-Yellow-BGGreen, .-Color-Cyan-BGGreen, .-Color-Green-BGGreen, .-Color-Magenta-BGGreen, .-Color-Red-BGGreen, .-Color-White-BGGreen, .-Color-Yellow-BGGreen { background-color: var(--ansi-green); } .-Color-BGYellow, .-Color-Black-BGYellow, .-Color-Blue-BGYellow, .-Color-Bold-BGYellow, .-Color-Bold-Black-BGYellow, .-Color-Bold-Green-BGYellow, .-Color-Bold-Cyan-BGYellow, .-Color-Bold-Blue-BGYellow, .-Color-Bold-Magenta-BGYellow, .-Color-Bold-Red-BGYellow, .-Color-Bold-White-BGYellow, .-Color-Bold-Yellow-BGYellow, .-Color-Cyan-BGYellow, .-Color-Green-BGYellow, .-Color-Magenta-BGYellow, .-Color-Red-BGYellow, .-Color-White-BGYellow, .-Color-Yellow-BGYellow { background-color: var(--ansi-yellow); } .-Color-BGBlue, .-Color-Black-BGBlue, .-Color-Blue-BGBlue, .-Color-Bold-BGBlue, .-Color-Bold-Black-BGBlue, .-Color-Bold-Green-BGBlue, .-Color-Bold-Cyan-BGBlue, .-Color-Bold-Blue-BGBlue, .-Color-Bold-Magenta-BGBlue, .-Color-Bold-Red-BGBlue, .-Color-Bold-White-BGBlue, .-Color-Bold-Yellow-BGBlue, .-Color-Cyan-BGBlue, .-Color-Green-BGBlue, .-Color-Magenta-BGBlue, .-Color-Red-BGBlue, .-Color-White-BGBlue, .-Color-Yellow-BGBlue { background-color: var(--ansi-blue); } .-Color-BGMagenta, .-Color-Black-BGMagenta, .-Color-Blue-BGMagenta, .-Color-Bold-BGMagenta, .-Color-Bold-Black-BGMagenta, .-Color-Bold-Green-BGMagenta, .-Color-Bold-Cyan-BGMagenta, .-Color-Bold-Blue-BGMagenta, .-Color-Bold-Magenta-BGMagenta, .-Color-Bold-Red-BGMagenta, .-Color-Bold-White-BGMagenta, .-Color-Bold-Yellow-BGMagenta, .-Color-Cyan-BGMagenta, .-Color-Green-BGMagenta, .-Color-Magenta-BGMagenta, .-Color-Red-BGMagenta, .-Color-White-BGMagenta, .-Color-Yellow-BGMagenta { background-color: var(--ansi-magenta); } .-Color-BGCyan, .-Color-Black-BGCyan, .-Color-Blue-BGCyan, .-Color-Bold-BGCyan, .-Color-Bold-Black-BGCyan, .-Color-Bold-Green-BGCyan, .-Color-Bold-Cyan-BGCyan, .-Color-Bold-Blue-BGCyan, .-Color-Bold-Magenta-BGCyan, .-Color-Bold-Red-BGCyan, .-Color-Bold-White-BGCyan, .-Color-Bold-Yellow-BGCyan, .-Color-Cyan-BGCyan, .-Color-Green-BGCyan, .-Color-Magenta-BGCyan, .-Color-Red-BGCyan, .-Color-White-BGCyan, .-Color-Yellow-BGCyan { background-color: var(--ansi-cyan); } .-Color-BGWhite, .-Color-Black-BGWhite, .-Color-Blue-BGWhite, .-Color-Bold-BGWhite, .-Color-Bold-Black-BGWhite, .-Color-Bold-Green-BGWhite, .-Color-Bold-Cyan-BGWhite, .-Color-Bold-Blue-BGWhite, .-Color-Bold-Magenta-BGWhite, .-Color-Bold-Red-BGWhite, .-Color-Bold-White-BGWhite, .-Color-Bold-Yellow-BGWhite, .-Color-Cyan-BGWhite, .-Color-Green-BGWhite, .-Color-Magenta-BGWhite, .-Color-Red-BGWhite, .-Color-White-BGWhite, .-Color-Yellow-BGWhite { background-color: var(--ansi-white); } .-Color-Black, .-Color-Bold-Black, .-Color-Black-BGBlack, .-Color-Bold-Black-BGBlack, .-Color-Black-BGGreen, .-Color-Red-BGRed, .-Color-Bold-Red-BGRed, .-Color-Bold-Blue-BGBlue, .-Color-Blue-BGBlue { text-shadow: 0 0 1px var(--ansi-white); } .-Color-Bold-Cyan-BGCyan, .-Color-Bold-Magenta-BGMagenta, .-Color-Bold-White, .-Color-Bold-Yellow-BGYellow, .-Color-Bold-Green-BGGreen, .-Color-Cyan-BGCyan, .-Color-Cyan-BGGreen, .-Color-Green-BGCyan, .-Color-Green-BGGreen, .-Color-Magenta-BGMagenta, .-Color-White, .-Color-White-BGWhite, .-Color-Yellow-BGYellow { text-shadow: 0 0 1px var(--ansi-black); }markdown-exec-1.8.0/src/markdown_exec/debug.py000066400000000000000000000052261454604644200213570ustar00rootroot00000000000000"""Debugging utilities.""" from __future__ import annotations import os import platform import sys from dataclasses import dataclass from importlib import metadata @dataclass class Variable: """Dataclass describing an environment variable.""" name: str """Variable name.""" value: str """Variable value.""" @dataclass class Package: """Dataclass describing a Python package.""" name: str """Package name.""" version: str """Package version.""" @dataclass class Environment: """Dataclass to store environment information.""" interpreter_name: str """Python interpreter name.""" interpreter_version: str """Python interpreter version.""" platform: str """Operating System.""" packages: list[Package] """Installed packages.""" variables: list[Variable] """Environment variables.""" def _interpreter_name_version() -> tuple[str, str]: if hasattr(sys, "implementation"): impl = sys.implementation.version version = f"{impl.major}.{impl.minor}.{impl.micro}" kind = impl.releaselevel if kind != "final": version += kind[0] + str(impl.serial) return sys.implementation.name, version return "", "0.0.0" def get_version(dist: str = "markdown-exec") -> str: """Get version of the given distribution. Parameters: dist: A distribution name. Returns: A version number. """ try: return metadata.version(dist) except metadata.PackageNotFoundError: return "0.0.0" def get_debug_info() -> Environment: """Get debug/environment information. Returns: Environment information. """ py_name, py_version = _interpreter_name_version() packages = ["markdown-exec"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MARKDOWN_EXEC")]] return Environment( interpreter_name=py_name, interpreter_version=py_version, platform=platform.platform(), variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], packages=[Package(pkg, get_version(pkg)) for pkg in packages], ) def print_debug_info() -> None: """Print debug/environment information.""" info = get_debug_info() print(f"- __System__: {info.platform}") print(f"- __Python__: {info.interpreter_name} {info.interpreter_version}") print("- __Environment variables__:") for var in info.variables: print(f" - `{var.name}`: `{var.value}`") print("- __Installed packages__:") for pkg in info.packages: print(f" - `{pkg.name}` v{pkg.version}") if __name__ == "__main__": print_debug_info() markdown-exec-1.8.0/src/markdown_exec/formatters/000077500000000000000000000000001454604644200221005ustar00rootroot00000000000000markdown-exec-1.8.0/src/markdown_exec/formatters/__init__.py000066400000000000000000000000631454604644200242100ustar00rootroot00000000000000"""This subpackage contains all the formatters.""" markdown-exec-1.8.0/src/markdown_exec/formatters/base.py000066400000000000000000000112311454604644200233620ustar00rootroot00000000000000"""Generic formatter for executing code.""" from __future__ import annotations from textwrap import indent from typing import TYPE_CHECKING, Any, Callable from uuid import uuid4 from markupsafe import Markup from markdown_exec.logger import get_logger from markdown_exec.rendering import MarkdownConverter, add_source, code_block if TYPE_CHECKING: from markdown.core import Markdown logger = get_logger(__name__) default_tabs = ("Source", "Result") class ExecutionError(Exception): """Exception raised for errors during execution of a code block. Attributes: message: The exception message. returncode: The code returned by the execution of the code block. """ def __init__(self, message: str, returncode: int | None = None) -> None: # noqa: D107 super().__init__(message) self.returncode = returncode def _format_log_details(details: str, *, strip_fences: bool = False) -> str: if strip_fences: lines = details.split("\n") if lines[0].startswith("```") and lines[-1].startswith("```"): details = "\n".join(lines[1:-1]) return indent(details, " " * 2) def base_format( *, language: str, run: Callable, code: str, md: Markdown, html: bool = False, source: str = "", result: str = "", tabs: tuple[str, str] = default_tabs, id: str = "", # noqa: A002 id_prefix: str | None = None, returncode: int = 0, transform_source: Callable[[str], tuple[str, str]] | None = None, session: str | None = None, update_toc: bool = True, **options: Any, ) -> Markup: """Execute code and return HTML. Parameters: language: The code language. run: Function that runs code and returns output. code: The code to execute. md: The Markdown instance. html: Whether to inject output as HTML directly, without rendering. source: Whether to show source as well, and where. result: If provided, use as language to format result in a code block. tabs: Titles of tabs (if used). id: An optional ID for the code block (useful when warning about errors). id_prefix: A string used to prefix HTML ids in the generated HTML. returncode: The expected exit code. transform_source: An optional callable that returns transformed versions of the source. The input source is the one that is ran, the output source is the one that is rendered (when the source option is enabled). session: A session name, to persist state between executed code blocks. update_toc: Whether to include generated headings into the Markdown table of contents (toc extension). **options: Additional options passed from the formatter. Returns: HTML contents. """ markdown = MarkdownConverter(md, update_toc=update_toc) extra = options.get("extra", {}) if transform_source: source_input, source_output = transform_source(code) else: source_input = code source_output = code try: output = run(source_input, returncode=returncode, session=session, id=id, **extra) except ExecutionError as error: identifier = id or extra.get("title", "") identifier = identifier and f"'{identifier}' " exit_message = "errors" if error.returncode is None else f"unexpected code {error.returncode}" log_message = ( f"Execution of {language} code block {identifier}exited with {exit_message}\n\n" f"Code block is:\n\n{_format_log_details(source_input)}\n\n" f"Output is:\n\n{_format_log_details(str(error), strip_fences=True)}\n" ) logger.warning(log_message) return markdown.convert(str(error)) if html: if source: placeholder = str(uuid4()) wrapped_output = add_source( source=source_output, location=source, output=placeholder, language=language, tabs=tabs, **extra, ) return markdown.convert(wrapped_output, stash={placeholder: output}) return Markup(output) wrapped_output = output if result and source != "console": wrapped_output = code_block(result, output) if source: wrapped_output = add_source( source=source_output, location=source, output=wrapped_output, language=language, tabs=tabs, result=result, **extra, ) prefix = id_prefix if id_prefix is not None else (f"{id}-" if id else None) return markdown.convert(wrapped_output, id_prefix=prefix) markdown-exec-1.8.0/src/markdown_exec/formatters/bash.py000066400000000000000000000015661454604644200233770ustar00rootroot00000000000000"""Formatter for executing shell code.""" from __future__ import annotations import subprocess from typing import Any from markdown_exec.formatters.base import ExecutionError, base_format from markdown_exec.rendering import code_block def _run_bash( code: str, returncode: int | None = None, session: str | None = None, # noqa: ARG001 id: str | None = None, # noqa: A002,ARG001 **extra: str, ) -> str: process = subprocess.run( ["bash", "-c", code], # noqa: S603,S607 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, ) if process.returncode != returncode: raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode) return process.stdout def _format_bash(**kwargs: Any) -> str: return base_format(language="bash", run=_run_bash, **kwargs) markdown-exec-1.8.0/src/markdown_exec/formatters/console.py000066400000000000000000000014571454604644200241230ustar00rootroot00000000000000"""Formatter for executing shell console code.""" from __future__ import annotations import textwrap from typing import TYPE_CHECKING, Any from markdown_exec.formatters.base import base_format from markdown_exec.formatters.sh import _run_sh from markdown_exec.logger import get_logger if TYPE_CHECKING: from markupsafe import Markup logger = get_logger(__name__) def _transform_source(code: str) -> tuple[str, str]: sh_lines = [] for line in code.split("\n"): prompt = line[:2] if prompt in {"$ ", "% "}: sh_lines.append(line[2:]) sh_code = "\n".join(sh_lines) return sh_code, textwrap.indent(sh_code, prompt) def _format_console(**kwargs: Any) -> Markup: return base_format(language="console", run=_run_sh, transform_source=_transform_source, **kwargs) markdown-exec-1.8.0/src/markdown_exec/formatters/markdown.py000066400000000000000000000004241454604644200242740ustar00rootroot00000000000000"""Formatter for literate Markdown.""" from __future__ import annotations from typing import Any from markdown_exec.formatters.base import base_format def _format_markdown(**kwargs: Any) -> str: return base_format(language="md", run=lambda code, **_: code, **kwargs) markdown-exec-1.8.0/src/markdown_exec/formatters/pycon.py000066400000000000000000000015261454604644200236060ustar00rootroot00000000000000"""Formatter for executing `pycon` code.""" from __future__ import annotations from typing import TYPE_CHECKING, Any from markdown_exec.formatters.base import base_format from markdown_exec.formatters.python import _run_python from markdown_exec.logger import get_logger if TYPE_CHECKING: from markupsafe import Markup logger = get_logger(__name__) def _transform_source(code: str) -> tuple[str, str]: python_lines = [] pycon_lines = [] for line in code.split("\n"): if line.startswith((">>> ", "... ")): pycon_lines.append(line) python_lines.append(line[4:]) python_code = "\n".join(python_lines) return python_code, "\n".join(pycon_lines) def _format_pycon(**kwargs: Any) -> Markup: return base_format(language="pycon", run=_run_python, transform_source=_transform_source, **kwargs) markdown-exec-1.8.0/src/markdown_exec/formatters/pyodide.py000066400000000000000000000055221454604644200241130ustar00rootroot00000000000000"""Formatter for creating a Pyodide interactive editor.""" from __future__ import annotations from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from markdown import Markdown play_emoji = '' clear_emoji = '' template = """
Editor (session: %(session)s)%(play_emoji)s Run
%(initial_code)s
Output%(clear_emoji)s Clear
""" _counter = 0 def _format_pyodide(code: str, md: Markdown, session: str, extra: dict, **options: Any) -> str: # noqa: ARG001 global _counter # noqa: PLW0603 _counter += 1 install = extra.pop("install", "") install = install.split(",") if install else [] theme = extra.pop("theme", "tomorrow,tomorrow_night") if "," not in theme: theme = f"{theme},{theme}" theme_light, theme_dark = theme.split(",") data = { "id_prefix": f"exec-{_counter}--", "initial_code": code, "install": install, "theme_light": theme_light.strip(), "theme_dark": theme_dark.strip(), "session": session or "default", "play_emoji": play_emoji, "clear_emoji": clear_emoji, } return template % data markdown-exec-1.8.0/src/markdown_exec/formatters/python.py000066400000000000000000000045461454604644200240040ustar00rootroot00000000000000"""Formatter for executing Python code.""" from __future__ import annotations import traceback from collections import defaultdict from functools import partial from io import StringIO from typing import Any from markdown_exec.formatters.base import ExecutionError, base_format from markdown_exec.rendering import code_block _sessions_globals: dict[str, dict] = defaultdict(dict) _sessions_counter: dict[str | None, int] = defaultdict(int) _code_blocks: dict[str, list[str]] = {} def _buffer_print(buffer: StringIO, *texts: str, end: str = "\n", **kwargs: Any) -> None: # noqa: ARG001 buffer.write(" ".join(str(text) for text in texts) + end) def _code_block_id( id: str | None = None, # noqa: A002 session: str | None = None, title: str | None = None, ) -> str: _sessions_counter[session] += 1 if id: code_block_id = f"id {id}" elif session: code_block_id = f"session {session}; n{_sessions_counter[session]}" if title: code_block_id = f"{code_block_id}; title {title}" else: code_block_id = f"n{_sessions_counter[session]}" if title: code_block_id = f"{code_block_id}; title {title}" return f"" def _run_python( code: str, returncode: int | None = None, # noqa: ARG001 session: str | None = None, id: str | None = None, # noqa: A002 **extra: str, ) -> str: title = extra.get("title", None) code_block_id = _code_block_id(id, session, title) _code_blocks[code_block_id] = code.split("\n") exec_globals = _sessions_globals[session] if session else {} buffer = StringIO() exec_globals["print"] = partial(_buffer_print, buffer) try: compiled = compile(code, filename=code_block_id, mode="exec") exec(compiled, exec_globals) # noqa: S102 except Exception as error: # noqa: BLE001 trace = traceback.TracebackException.from_exception(error) for frame in trace.stack: if frame.filename.startswith(" str: return base_format(language="python", run=_run_python, **kwargs) markdown-exec-1.8.0/src/markdown_exec/formatters/sh.py000066400000000000000000000015541454604644200230710ustar00rootroot00000000000000"""Formatter for executing shell code.""" from __future__ import annotations import subprocess from typing import Any from markdown_exec.formatters.base import ExecutionError, base_format from markdown_exec.rendering import code_block def _run_sh( code: str, returncode: int | None = None, session: str | None = None, # noqa: ARG001 id: str | None = None, # noqa: A002,ARG001 **extra: str, ) -> str: process = subprocess.run( ["sh", "-c", code], # noqa: S603,S607 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, ) if process.returncode != returncode: raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode) return process.stdout def _format_sh(**kwargs: Any) -> str: return base_format(language="sh", run=_run_sh, **kwargs) markdown-exec-1.8.0/src/markdown_exec/formatters/tree.py000066400000000000000000000037761454604644200234260ustar00rootroot00000000000000"""Formatter for file-system trees.""" from __future__ import annotations from textwrap import dedent from typing import TYPE_CHECKING, Any from markdown_exec.rendering import MarkdownConverter, code_block if TYPE_CHECKING: from markdown import Markdown def _rec_build_tree(lines: list[str], parent: list, offset: int, base_indent: int) -> int: while offset < len(lines): line = lines[offset] lstripped = line.lstrip() indent = len(line) - len(lstripped) if indent == base_indent: parent.append((lstripped, [])) offset += 1 elif indent > base_indent: offset = _rec_build_tree(lines, parent[-1][1], offset, indent) else: return offset return offset def _build_tree(code: str) -> list[tuple[str, list]]: lines = dedent(code.strip()).split("\n") root_layer: list[tuple[str, list]] = [] _rec_build_tree(lines, root_layer, 0, 0) return root_layer def _rec_format_tree(tree: list[tuple[str, list]], *, root: bool = True) -> list[str]: lines = [] n_items = len(tree) for index, node in enumerate(tree): last = index == n_items - 1 prefix = "" if root else f"{'└' if last else '├'}── " if node[1]: lines.append(f"{prefix}📁 {node[0]}") sublines = _rec_format_tree(node[1], root=False) if root: lines.extend(sublines) else: indent_char = " " if last else "│" lines.extend([f"{indent_char} {line}" for line in sublines]) else: name = node[0].split()[0] icon = "📁" if name.endswith("/") else "📄" lines.append(f"{prefix}{icon} {node[0]}") return lines def _format_tree(code: str, md: Markdown, result: str, **options: Any) -> str: markdown = MarkdownConverter(md) output = "\n".join(_rec_format_tree(_build_tree(code))) return markdown.convert(code_block(result or "bash", output, **options.get("extra", {}))) markdown-exec-1.8.0/src/markdown_exec/logger.py000066400000000000000000000040751454604644200215510ustar00rootroot00000000000000"""This module contains logging utilities. We provide the [`patch_loggers`][markdown_exec.logger.patch_loggers] function so dependant libraries can patch loggers as they see fit. For example, to fit in the MkDocs logging configuration and prefix each log message with the module name: ```python import logging from markdown_exec.logger import patch_loggers class LoggerAdapter(logging.LoggerAdapter): def __init__(self, prefix, logger): super().__init__(logger, {}) self.prefix = prefix def process(self, msg, kwargs): return f"{self.prefix}: {msg}", kwargs def get_logger(name): logger = logging.getLogger(f"mkdocs.plugins.{name}") return LoggerAdapter(name.split(".", 1)[0], logger) patch_loggers(get_logger) ``` """ from __future__ import annotations import logging from typing import Any, Callable, ClassVar class _Logger: _default_logger: Any = logging.getLogger _instances: ClassVar[dict[str, _Logger]] = {} def __init__(self, name: str) -> None: # default logger that can be patched by third-party self._logger = self.__class__._default_logger(name) # register instance self._instances[name] = self def __getattr__(self, name: str) -> Any: # forward everything to the logger return getattr(self._logger, name) @classmethod def _patch_loggers(cls, get_logger_func: Callable) -> None: # patch current instances for name, instance in cls._instances.items(): instance._logger = get_logger_func(name) # future instances will be patched as well cls._default_logger = get_logger_func def get_logger(name: str) -> _Logger: """Create and return a new logger instance. Parameters: name: The logger name. Returns: The logger. """ return _Logger(name) def patch_loggers(get_logger_func: Callable[[str], Any]) -> None: """Patch loggers. Parameters: get_logger_func: A function accepting a name as parameter and returning a logger. """ _Logger._patch_loggers(get_logger_func) markdown-exec-1.8.0/src/markdown_exec/mkdocs_plugin.py000066400000000000000000000111351454604644200231230ustar00rootroot00000000000000"""This module contains an optional plugin for MkDocs.""" from __future__ import annotations import logging import os from pathlib import Path from typing import TYPE_CHECKING, Any, MutableMapping from mkdocs.config import config_options from mkdocs.config.base import Config from mkdocs.plugins import BasePlugin from mkdocs.utils import write_file from markdown_exec import formatter, formatters, validator from markdown_exec.logger import patch_loggers from markdown_exec.rendering import MarkdownConverter, markdown_config if TYPE_CHECKING: from jinja2 import Environment from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import Files try: __import__("pygments_ansi_color") except ImportError: ansi_ok = False else: ansi_ok = True class _LoggerAdapter(logging.LoggerAdapter): def __init__(self, prefix: str, logger: logging.Logger) -> None: super().__init__(logger, {}) self.prefix = prefix def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]: return f"{self.prefix}: {msg}", kwargs def _get_logger(name: str) -> _LoggerAdapter: logger = logging.getLogger(f"mkdocs.plugins.{name}") return _LoggerAdapter(name.split(".", 1)[0], logger) patch_loggers(_get_logger) class MarkdownExecPluginConfig(Config): """Configuration of the plugin (for `mkdocs.yml`).""" ansi = config_options.Choice(("auto", "off", "required", True, False), default="auto") """Whether the `ansi` extra is required when installing the package.""" languages = config_options.ListOfItems( config_options.Choice(formatters.keys()), default=list(formatters.keys()), ) """Which languages to enabled the extension for.""" class MarkdownExecPlugin(BasePlugin[MarkdownExecPluginConfig]): """MkDocs plugin to easily enable custom fences for code blocks execution.""" def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: """Configure the plugin. Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). In this hook, we add custom fences for all the supported languages. We also save the Markdown extensions configuration into [`markdown_config`][markdown_exec.rendering.markdown_config]. Arguments: config: The MkDocs config object. Returns: The modified config. """ self.mkdocs_config_dir = os.getenv("MKDOCS_CONFIG_DIR") os.environ["MKDOCS_CONFIG_DIR"] = os.path.dirname(config["config_file_path"]) self.languages = self.config.languages mdx_configs = config.setdefault("mdx_configs", {}) superfences = mdx_configs.setdefault("pymdownx.superfences", {}) custom_fences = superfences.setdefault("custom_fences", []) for language in self.languages: custom_fences.append( { "name": language, "class": language, "validator": validator, "format": formatter, }, ) markdown_config.save(config.markdown_extensions, config.mdx_configs) return config def on_env( # noqa: D102 self, env: Environment, *, config: MkDocsConfig, files: Files, # noqa: ARG002 ) -> Environment | None: if self.config.ansi in ("required", True) or (self.config.ansi == "auto" and ansi_ok): self._add_css(config, "ansi.css") if "pyodide" in self.languages: self._add_css(config, "pyodide.css") self._add_js(config, "pyodide.js") return env def on_post_build(self, *, config: MkDocsConfig) -> None: # noqa: ARG002,D102 MarkdownConverter.counter = 0 markdown_config.reset() if self.mkdocs_config_dir is None: os.environ.pop("MKDOCS_CONFIG_DIR", None) else: os.environ["MKDOCS_CONFIG_DIR"] = self.mkdocs_config_dir def _add_asset(self, config: MkDocsConfig, asset_file: str, asset_type: str) -> None: asset_filename = f"assets/_markdown_exec_{asset_file}" asset_content = Path(__file__).parent.joinpath(asset_file).read_text() write_file(asset_content.encode("utf-8"), os.path.join(config.site_dir, asset_filename)) config[f"extra_{asset_type}"].insert(0, asset_filename) def _add_css(self, config: MkDocsConfig, css_file: str) -> None: self._add_asset(config, css_file, "css") def _add_js(self, config: MkDocsConfig, js_file: str) -> None: self._add_asset(config, js_file, "javascript") markdown-exec-1.8.0/src/markdown_exec/processors.py000066400000000000000000000103031454604644200224630ustar00rootroot00000000000000"""This module contains a Markdown extension allowing to integrate generated headings into the ToC.""" from __future__ import annotations import copy import re from typing import TYPE_CHECKING from xml.etree.ElementTree import Element from markdown.treeprocessors import Treeprocessor from markdown.util import HTML_PLACEHOLDER_RE if TYPE_CHECKING: from markdown import Markdown from markupsafe import Markup # code taken from mkdocstrings, credits to @oprypin class IdPrependingTreeprocessor(Treeprocessor): """Prepend the configured prefix to IDs of all HTML elements.""" name = "markdown_exec_ids" def __init__(self, md: Markdown, id_prefix: str) -> None: # noqa: D107 super().__init__(md) self.id_prefix = id_prefix def run(self, root: Element) -> None: # noqa: D102 if not self.id_prefix: return for el in root.iter(): id_attr = el.get("id") if id_attr: el.set("id", self.id_prefix + id_attr) href_attr = el.get("href") if href_attr and href_attr.startswith("#"): el.set("href", "#" + self.id_prefix + href_attr[1:]) name_attr = el.get("name") if name_attr: el.set("name", self.id_prefix + name_attr) if el.tag == "label": for_attr = el.get("for") if for_attr: el.set("for", self.id_prefix + for_attr) # code taken from mkdocstrings, credits to @oprypin class HeadingReportingTreeprocessor(Treeprocessor): """Records the heading elements encountered in the document.""" name = "markdown_exec_record_headings" regex = re.compile("[Hh][1-6]") def __init__(self, md: Markdown, headings: list[Element]): # noqa: D107 super().__init__(md) self.headings = headings def run(self, root: Element) -> None: # noqa: D102 for el in root.iter(): if self.regex.fullmatch(el.tag): el = copy.copy(el) # noqa: PLW2901 # 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML. # Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension. if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: # type: ignore[attr-defined] del el[-1] self.headings.append(el) class InsertHeadings(Treeprocessor): """Our headings insertor.""" name = "markdown_exec_insert_headings" def __init__(self, md: Markdown): """Initialize the object. Arguments: md: A `markdown.Markdown` instance. """ super().__init__(md) self.headings: dict[Markup, list[Element]] = {} def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring) if not self.headings: return for el in root.iter(): match = HTML_PLACEHOLDER_RE.match(el.text or "") if match: counter = int(match.group(1)) markup: Markup = self.md.htmlStash.rawHtmlBlocks[counter] # type: ignore[assignment] if markup in self.headings: div = Element("div", {"class": "markdown-exec"}) div.extend(self.headings[markup]) el.append(div) class RemoveHeadings(Treeprocessor): """Our headings remover.""" name = "markdown_exec_remove_headings" def run(self, root: Element) -> None: # noqa: D102 carry_text = "" for el in reversed(root): # Reversed mainly for the ability to mutate during iteration. for subel in reversed(el): if subel.tag == "div" and subel.get("class") == "markdown-exec": # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML). carry_text = (subel.text or "") + carry_text el.remove(subel) elif carry_text: subel.tail = (subel.tail or "") + carry_text carry_text = "" if carry_text: el.text = (el.text or "") + carry_text markdown-exec-1.8.0/src/markdown_exec/py.typed000066400000000000000000000000001454604644200213770ustar00rootroot00000000000000markdown-exec-1.8.0/src/markdown_exec/pyodide.css000066400000000000000000000015211454604644200220600ustar00rootroot00000000000000html[data-theme="light"] { @import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.css" } html[data-theme="dark"] { @import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css" } .ace_gutter { z-index: 1; } .pyodide-editor { width: 100%; min-height: 200px; max-height: 400px; font-size: .85em; } .pyodide-editor-bar { color: var(--md-primary-bg-color); background-color: var(--md-primary-fg-color); width: 100%; font: monospace; font-size: 0.75em; padding: 2px 0 2px; } .pyodide-bar-item { padding: 0 18px 0; display: inline-block; width: 50%; } .pyodide pre { margin: 0; } .pyodide-output { width: 100%; margin-bottom: -15px; max-height: 400px } .pyodide-clickable { cursor: pointer; text-align: right; }markdown-exec-1.8.0/src/markdown_exec/pyodide.js000066400000000000000000000074051454604644200217130ustar00rootroot00000000000000var _sessions = {}; function getSession(name, pyodide) { if (!(name in _sessions)) { _sessions[name] = pyodide.globals.get("dict")(); } return _sessions[name]; } function writeOutput(element, string) { element.innerHTML += string + '\n'; } function clearOutput(element) { element.innerHTML = ''; } async function evaluatePython(pyodide, editor, output, session) { pyodide.setStdout({ batched: (string) => { writeOutput(output, string); } }); let result, code = editor.getValue(); clearOutput(output); try { result = await pyodide.runPythonAsync(code, { globals: getSession(session, pyodide) }); } catch (error) { writeOutput(output, error); } if (result) writeOutput(output, result); hljs.highlightElement(output); } async function initPyodide() { try { let pyodide = await loadPyodide(); await pyodide.loadPackage("micropip"); return pyodide; } catch(error) { return null; } } function getTheme() { return document.body.getAttribute('data-md-color-scheme'); } function setTheme(editor, currentTheme, light, dark) { // https://gist.github.com/RyanNutt/cb8d60997d97905f0b2aea6c3b5c8ee0 if (currentTheme === "default") { editor.setTheme("ace/theme/" + light); document.querySelector(`link[title="light"]`).removeAttribute("disabled"); document.querySelector(`link[title="dark"]`).setAttribute("disabled", "disabled"); } else if (currentTheme === "slate") { editor.setTheme("ace/theme/" + dark); document.querySelector(`link[title="dark"]`).removeAttribute("disabled"); document.querySelector(`link[title="light"]`).setAttribute("disabled", "disabled"); } } function updateTheme(editor, light, dark) { // Create a new MutationObserver instance const observer = new MutationObserver((mutations) => { // Loop through the mutations that occurred mutations.forEach((mutation) => { // Check if the mutation was a change to the data-md-color-scheme attribute if (mutation.attributeName === 'data-md-color-scheme') { // Get the new value of the attribute const newColorScheme = mutation.target.getAttribute('data-md-color-scheme'); // Update the editor theme setTheme(editor, newColorScheme, light, dark); } }); }); // Configure the observer to watch for changes to the data-md-color-scheme attribute observer.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'], }); } async function setupPyodide(idPrefix, install = null, themeLight = 'tomorrow', themeDark = 'tomorrow_night', session = null) { const editor = ace.edit(idPrefix + "editor"); const run = document.getElementById(idPrefix + "run"); const clear = document.getElementById(idPrefix + "clear"); const output = document.getElementById(idPrefix + "output"); updateTheme(editor, themeLight, themeDark); editor.session.setMode("ace/mode/python"); setTheme(editor, getTheme(), themeLight, themeDark); writeOutput(output, "Initializing..."); let pyodide = await pyodidePromise; if (install && install.length) { micropip = pyodide.pyimport("micropip"); for (const package of install) await micropip.install(package); } clearOutput(output); run.onclick = () => evaluatePython(pyodide, editor, output, session); clear.onclick = () => clearOutput(output); output.parentElement.parentElement.addEventListener("keydown", (event) => { if (event.ctrlKey && event.key.toLowerCase() === 'enter') { event.preventDefault(); run.click(); } }); } var pyodidePromise = initPyodide(); markdown-exec-1.8.0/src/markdown_exec/rendering.py000066400000000000000000000223631454604644200222470ustar00rootroot00000000000000"""Markdown extensions and helpers.""" from __future__ import annotations from contextlib import contextmanager from functools import lru_cache from textwrap import indent from typing import TYPE_CHECKING, Any, Iterator from markdown import Markdown from markupsafe import Markup from markdown_exec.processors import ( HeadingReportingTreeprocessor, IdPrependingTreeprocessor, InsertHeadings, RemoveHeadings, ) if TYPE_CHECKING: from xml.etree.ElementTree import Element from markdown import Extension def code_block(language: str, code: str, **options: str) -> str: """Format code as a code block. Parameters: language: The code block language. code: The source code to format. **options: Additional options passed from the source, to add back to the generated code block. Returns: The formatted code block. """ opts = " ".join(f'{opt_name}="{opt_value}"' for opt_name, opt_value in options.items()) return f"````````{language} {opts}\n{code}\n````````" def tabbed(*tabs: tuple[str, str]) -> str: """Format tabs using `pymdownx.tabbed` extension. Parameters: *tabs: Tuples of strings: title and text. Returns: The formatted tabs. """ parts = [] for title, text in tabs: title = title.replace(r"\|", "|").strip() # noqa: PLW2901 parts.append(f'=== "{title}"') parts.append(indent(text, prefix=" " * 4)) parts.append("") return "\n".join(parts) def _hide_lines(source: str) -> str: return "\n".join(line for line in source.split("\n") if "markdown-exec: hide" not in line).strip() def add_source( *, source: str, location: str, output: str, language: str, tabs: tuple[str, str], result: str = "", **extra: str, ) -> str: """Add source code block to the output. Parameters: source: The source code block. location: Where to add the source (above, below, tabbed-left, tabbed-right, console). output: The current output. language: The code language. tabs: Tabs titles (if used). result: Syntax to use when concatenating source and result with "console" location. **extra: Extra options added back to source code block. Raises: ValueError: When the given location is not supported. Returns: The updated output. """ source = _hide_lines(source) if location == "console": return code_block(result or language, source + "\n" + output, **extra) source_block = code_block(language, source, **extra) if location == "above": return source_block + "\n\n" + output if location == "below": return output + "\n\n" + source_block if location == "material-block": return source_block + f'\n\n
\n\n{output}\n\n
' source_tab_title, result_tab_title = tabs if location == "tabbed-left": return tabbed((source_tab_title, source_block), (result_tab_title, output)) if location == "tabbed-right": return tabbed((result_tab_title, output), (source_tab_title, source_block)) raise ValueError(f"unsupported location for sources: {location}") class MarkdownConfig: """This class returns a singleton used to store Markdown extensions configuration. You don't have to instantiate the singleton yourself: we provide it as [`markdown_config`][markdown_exec.rendering.markdown_config]. """ _singleton: MarkdownConfig | None = None def __new__(cls) -> MarkdownConfig: # noqa: D102,PYI034 if cls._singleton is None: cls._singleton = super().__new__(cls) return cls._singleton def __init__(self) -> None: # noqa: D107 self.exts: list[str] | None = None self.exts_config: dict[str, dict[str, Any]] | None = None def save(self, exts: list[str], exts_config: dict[str, dict[str, Any]]) -> None: """Save Markdown extensions and their configuration. Parameters: exts: The Markdown extensions. exts_config: The extensions configuration. """ self.exts = exts self.exts_config = exts_config def reset(self) -> None: """Reset Markdown extensions and their configuration.""" self.exts = None self.exts_config = None markdown_config = MarkdownConfig() """This object can be used to save the configuration of your Markdown extensions. For example, since we provide a MkDocs plugin, we use it to store the configuration that was read from `mkdocs.yml`: ```python from markdown_exec.rendering import markdown_config # ...in relevant events/hooks, access and modify extensions and their configs, then: markdown_config.save(extensions, extensions_config) ``` See the actual event hook: [`on_config`][markdown_exec.mkdocs_plugin.MarkdownExecPlugin.on_config]. See the [`save`][markdown_exec.rendering.MarkdownConfig.save] and [`reset`][markdown_exec.rendering.MarkdownConfig.reset] methods. Without it, Markdown Exec will rely on the `registeredExtensions` attribute of the original Markdown instance, which does not forward everything that was configured, notably extensions like `tables`. Other extensions such as `attr_list` are visible, but fail to register properly when reusing their instances. It means that the rendered HTML might differ from what you expect (tables not rendered, attribute lists not injected, emojis not working, etc.). """ # FIXME: When a heading contains an XML entity such as —, # the entity is stashed and replaced with a placeholder. # The heading therefore contains this placeholder. # When reporting the heading to the upper conversion layer (for the ToC), # the placeholder gets unstashed using the upper Markdown instance # instead of the neste one. If the upper instance doesn't know the placeholder, # nothing happens. But if it knows it, we then get a heading with garbabe/previous # contents within it, messing up the ToC. # We should fix this somehow. In the meantime, the workaround is to avoid # XML entities that get stashed in headings. @lru_cache(maxsize=None) def _register_headings_processors(md: Markdown) -> None: md.treeprocessors.register( InsertHeadings(md), InsertHeadings.name, priority=75, # right before markdown.blockprocessors.HashHeaderProcessor ) md.treeprocessors.register( RemoveHeadings(md), RemoveHeadings.name, priority=4, # right after toc ) def _mimic(md: Markdown, headings: list[Element], *, update_toc: bool = True) -> Markdown: new_md = Markdown() extensions: list[Extension | str] = markdown_config.exts or md.registeredExtensions # type: ignore[assignment] extensions_config: dict[str, dict[str, Any]] = markdown_config.exts_config or {} new_md.registerExtensions(extensions, extensions_config) new_md.treeprocessors.register( IdPrependingTreeprocessor(md, ""), IdPrependingTreeprocessor.name, priority=4, # right after 'toc' (needed because that extension adds ids to headings) ) new_md._original_md = md # type: ignore[attr-defined] if update_toc: _register_headings_processors(md) new_md.treeprocessors.register( HeadingReportingTreeprocessor(new_md, headings), HeadingReportingTreeprocessor.name, priority=1, # Close to the end. ) return new_md @contextmanager def _id_prefix(md: Markdown, prefix: str | None) -> Iterator[None]: MarkdownConverter.counter += 1 id_prepending_processor = md.treeprocessors[IdPrependingTreeprocessor.name] id_prepending_processor.id_prefix = prefix if prefix is not None else f"exec-{MarkdownConverter.counter}--" # type: ignore[attr-defined] try: yield finally: id_prepending_processor.id_prefix = "" # type: ignore[attr-defined] class MarkdownConverter: """Helper class to avoid breaking the original Markdown instance state.""" counter: int = 0 def __init__(self, md: Markdown, *, update_toc: bool = True) -> None: # noqa: D107 self._md_ref: Markdown = md self._headings: list[Element] = [] self._update_toc = update_toc @property def _original_md(self) -> Markdown: return getattr(self._md_ref, "_original_md", self._md_ref) def _report_headings(self, markup: Markup) -> None: self._original_md.treeprocessors[InsertHeadings.name].headings[markup] = self._headings # type: ignore[attr-defined] self._headings = [] def convert(self, text: str, stash: dict[str, str] | None = None, id_prefix: str | None = None) -> Markup: """Convert Markdown text to safe HTML. Parameters: text: Markdown text. stash: An HTML stash. Returns: Safe HTML. """ md = _mimic(self._original_md, self._headings, update_toc=self._update_toc) # convert markdown to html with _id_prefix(md, id_prefix): converted = md.convert(text) # restore html from stash for placeholder, stashed in (stash or {}).items(): converted = converted.replace(placeholder, stashed) markup = Markup(converted) # pass headings to upstream conversion layer if self._update_toc: self._report_headings(markup) return markup markdown-exec-1.8.0/tests/000077500000000000000000000000001454604644200154375ustar00rootroot00000000000000markdown-exec-1.8.0/tests/__init__.py000066400000000000000000000002461454604644200175520ustar00rootroot00000000000000"""Tests suite for `markdown_exec`.""" from pathlib import Path TESTS_DIR = Path(__file__).parent TMP_DIR = TESTS_DIR / "tmp" FIXTURES_DIR = TESTS_DIR / "fixtures" markdown-exec-1.8.0/tests/conftest.py000066400000000000000000000011731454604644200176400ustar00rootroot00000000000000"""Configuration for the pytest test suite.""" import pytest from markdown import Markdown from markdown_exec import formatter, formatters, validator @pytest.fixture() def md() -> Markdown: """Return a Markdown instance. Returns: Markdown instance. """ fences = [ { "name": language, "class": language, "validator": validator, "format": formatter, } for language in formatters ] return Markdown( extensions=["pymdownx.superfences"], extension_configs={"pymdownx.superfences": {"custom_fences": fences}}, ) markdown-exec-1.8.0/tests/test_base_formatter.py000066400000000000000000000026431454604644200220520ustar00rootroot00000000000000"""Tests for the base formatter.""" import pytest from markdown import Markdown from markdown_exec.formatters.base import base_format def test_no_p_around_html(md: Markdown) -> None: """Assert HTML isn't wrapped in a `p` tag. Parameters: md: A Markdown instance (fixture). """ code = "
hello
" html = base_format( language="whatever", run=lambda code, **_: code, code=code, md=md, html=True, ) assert html == code @pytest.mark.parametrize("html", [True, False]) def test_render_source(md: Markdown, html: bool) -> None: """Assert source is rendered. Parameters: md: A Markdown instance (fixture). html: Whether output is HTML or not. """ markup = base_format( language="python", run=lambda code, **_: code, code="hello", md=md, html=html, source="tabbed-left", ) assert "Source" in markup def test_render_console_plus_ansi_result(md: Markdown) -> None: """Assert we can render source as console style with `ansi` highlight. Parameters: md: A Markdown instance (fixture). """ markup = base_format( language="bash", run=lambda code, **_: code, code="echo -e '\033[31mhello'", md=md, html=False, source="console", result="ansi", ) assert "ansi" in markup markdown-exec-1.8.0/tests/test_converter.py000066400000000000000000000040401454604644200210550ustar00rootroot00000000000000"""Tests for the Markdown converter.""" from __future__ import annotations import re from textwrap import dedent from typing import TYPE_CHECKING import pytest from markdown.extensions.toc import TocExtension from markdown_exec.rendering import MarkdownConfig, markdown_config if TYPE_CHECKING: from markdown import Markdown def test_rendering_nested_blocks(md: Markdown) -> None: """Assert nested blocks are properly handled. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ````md exec="1" ```python exec="1" print("**Bold!**") ``` ```` """, ), ) assert html == "

Bold!

" def test_instantiating_config_singleton() -> None: """Assert that the Markdown config instances act as a singleton.""" assert MarkdownConfig() is markdown_config markdown_config.save([], {}) markdown_config.reset() @pytest.mark.parametrize( ("id", "id_prefix", "expected"), [ ("", None, 'id="exec-\\d+--heading"'), ("", "", 'id="heading"'), ("", "some-prefix-", 'id="some-prefix-heading"'), ("some-id", None, 'id="some-id-heading"'), ("some-id", "", 'id="heading"'), ("some-id", "some-prefix-", 'id="some-prefix-heading"'), ], ) def test_prefixing_headings(md: Markdown, id: str, id_prefix: str | None, expected: str) -> None: # noqa: A002 """Assert that we prefix headings as specified. Parameters: md: A Markdown instance (fixture). id: The code block id. id_prefix: The code block id prefix. expected: The id we expect to find in the HTML. """ TocExtension().extendMarkdown(md) prefix = f'idprefix="{id_prefix}"' if id_prefix is not None else "" html = md.convert( dedent( f""" ```python exec="1" id="{id}" {prefix} print("# HEADING") ``` """, ), ) assert re.search(expected, html) markdown-exec-1.8.0/tests/test_python.py000066400000000000000000000103611454604644200203720ustar00rootroot00000000000000"""Tests for the Python formatters.""" from __future__ import annotations from textwrap import dedent from typing import TYPE_CHECKING if TYPE_CHECKING: import pytest from markdown import Markdown def test_output_markdown(md: Markdown) -> None: """Assert Markdown is converted to HTML. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```python exec="yes" print("**Bold!**") ``` """, ), ) assert html == "

Bold!

" def test_output_html(md: Markdown) -> None: """Assert HTML is injected as is. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```python exec="yes" html="yes" print("**Bold!**") ``` """, ), ) assert html == "

**Bold!**\n

" def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None: """Assert errors properly log a warning and return a formatted traceback. Parameters: md: A Markdown instance (fixture). caplog: Pytest fixture to capture logs. """ html = md.convert( dedent( """ ```python exec="yes" raise ValueError("oh no!") ``` """, ), ) assert "Traceback" in html assert "ValueError" in html assert "oh no!" in html assert "Execution of python code block exited with errors" in caplog.text def test_can_print_non_string_objects(md: Markdown) -> None: """Assert we can print non-string objects. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```python exec="yes" class NonString: def __str__(self): return "string" nonstring = NonString() print(nonstring, nonstring) ``` """, ), ) assert "Traceback" not in html def test_sessions(md: Markdown) -> None: """Assert sessions can be reused. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```python exec="1" session="a" a = 1 ``` ```pycon exec="1" session="b" >>> b = 2 ``` ```pycon exec="1" session="a" >>> print(f"a = {a}") >>> try: ... print(b) ... except NameError: ... print("ok") ... else: ... print("ko") ``` ```python exec="1" session="b" print(f"b = {b}") try: print(a) except NameError: print("ok") else: print("ko") ``` """, ), ) assert "a = 1" in html assert "b = 2" in html assert "ok" in html assert "ko" not in html def test_reporting_errors_in_sessions(md: Markdown, caplog: pytest.LogCaptureFixture) -> None: """Assert errors and source lines are correctly reported across sessions. Parameters: md: A Markdown instance (fixture). caplog: Pytest fixture to capture logs. """ html = md.convert( dedent( """ ```python exec="1" session="a" def fraise(): raise RuntimeError("strawberry") ``` ```python exec="1" session="a" print("hello") fraise() ``` """, ), ) assert "Traceback" in html assert "strawberry" in html assert "fraise()" in caplog.text assert 'raise RuntimeError("strawberry")' in caplog.text def test_removing_output_from_pycon_code(md: Markdown) -> None: """Assert output lines are removed from pycon snippets. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```pycon exec="1" source="console" >>> print("ok") ko ``` """, ), ) assert "ok" in html assert "ko" not in html markdown-exec-1.8.0/tests/test_shell.py000066400000000000000000000037531454604644200201670ustar00rootroot00000000000000"""Tests for the shell formatters.""" from __future__ import annotations from textwrap import dedent from typing import TYPE_CHECKING if TYPE_CHECKING: import pytest from markdown import Markdown def test_output_markdown(md: Markdown) -> None: """Assert Markdown is converted to HTML. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```sh exec="yes" echo "**Bold!**" ``` """, ), ) assert html == "

Bold!

" def test_output_html(md: Markdown) -> None: """Assert HTML is injected as is. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```sh exec="yes" html="yes" echo "**Bold!**" ``` """, ), ) assert html == "

**Bold!**\n

" def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None: """Assert errors properly log a warning and return a formatted traceback. Parameters: md: A Markdown instance (fixture). caplog: Pytest fixture to capture logs. """ html = md.convert( dedent( """ ```sh exec="yes" echo("wrong syntax") ``` """, ), ) assert "error" in html assert "Execution of sh code block exited with unexpected code 2" in caplog.text def test_return_code(md: Markdown, caplog: pytest.LogCaptureFixture) -> None: """Assert return code is used correctly. Parameters: md: A Markdown instance (fixture). caplog: Pytest fixture to capture logs. """ html = md.convert( dedent( """ ```sh exec="yes" returncode="1" echo Not in the mood exit 1 ``` """, ), ) assert "Not in the mood" in html assert "exited with" not in caplog.text markdown-exec-1.8.0/tests/test_toc.py000066400000000000000000000044371454604644200176450ustar00rootroot00000000000000"""Tests for the logic updating the table of contents.""" from __future__ import annotations from textwrap import dedent from typing import TYPE_CHECKING from markdown.extensions.toc import TocExtension if TYPE_CHECKING: from markdown import Markdown def test_updating_toc(md: Markdown) -> None: """Assert ToC is updated with generated headings. Parameters: md: A Markdown instance (fixture). """ TocExtension().extendMarkdown(md) html = md.convert( dedent( """ ```python exec="yes" print("# big heading") ``` """, ), ) assert " None: """Assert ToC is not updated with generated headings. Parameters: md: A Markdown instance (fixture). """ TocExtension().extendMarkdown(md) html = md.convert( dedent( """ ```python exec="yes" updatetoc="no" print("# big heading") ``` """, ), ) assert " None: """Assert ToC is not updated with generated headings. Parameters: md: A Markdown instance (fixture). """ TocExtension().extendMarkdown(md) html = md.convert( dedent( """ ```python exec="yes" updatetoc="no" print("# big heading") ``` ```python exec="yes" updatetoc="yes" print("## medium heading") ``` ```python exec="yes" updatetoc="no" print("### small heading") ``` ```python exec="yes" updatetoc="yes" print("#### tiny heading") ``` """, ), ) assert " None: """Assert we can highlight lines in the output. Parameters: md: A Markdown instance (fixture). """ html = md.convert( dedent( """ ```tree hl_lines="2" 1 2 3 ``` """, ), ) assert '' in html markdown-exec-1.8.0/tests/test_validator.py000066400000000000000000000017431454604644200210420ustar00rootroot00000000000000"""Tests for the `validator` function.""" import pytest from markdown.core import Markdown from markdown_exec import validator @pytest.mark.parametrize( ("exec_value", "expected"), [ ("yes", True), ("YES", True), ("on", True), ("ON", True), ("whynot", True), ("true", True), ("TRUE", True), ("1", True), ("-1", True), ("0", False), ("no", False), ("NO", False), ("off", False), ("OFF", False), ("false", False), ("FALSE", False), ], ) def test_validate(md: Markdown, exec_value: str, expected: bool) -> None: """Assert the validator returns True or False given inputs. Parameters: md: A Markdown instance. exec_value: The exec option value, passed from the code block. expected: Expected validation result. """ assert validator("whatever", inputs={"exec": exec_value}, options={}, attrs={}, md=md) is expected