├── .copier-answers.yml ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-test.yaml ├── .yamllint.yaml ├── AGENTS.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── mdformat_mkdocs ├── __init__.py ├── _helpers.py ├── _normalize_list.py ├── _postprocess_inline.py ├── _synced │ ├── __init__.py │ └── admon_factories │ │ ├── README.md │ │ ├── __init__.py │ │ └── _whitespace_admon_factories.py ├── mdit_plugins │ ├── __init__.py │ ├── _material_admon.py │ ├── _material_content_tabs.py │ ├── _material_deflist.py │ ├── _mkdocstrings_autorefs.py │ ├── _mkdocstrings_crossreference.py │ ├── _pymd_abbreviations.py │ ├── _pymd_admon.py │ ├── _pymd_arithmatex.py │ ├── _pymd_captions.py │ ├── _pymd_snippet.py │ └── _python_markdown_attr_list.py ├── plugin.py └── py.typed ├── mdsf.json ├── mise.lock ├── mise.toml ├── pyproject.toml └── tests ├── __init__.py ├── format ├── __init__.py ├── __snapshots__ │ └── test_parsed_result.ambr ├── fixtures │ ├── material_content_tabs.md │ ├── material_deflist.md │ ├── material_math.md │ ├── math_with_mkdocs_features.md │ ├── mkdocstrings_autorefs.md │ ├── parsed_result.md │ ├── pymd_abbreviations.md │ ├── pymd_arithmatex.md │ ├── pymd_arithmatex_ams_environments.md │ ├── pymd_arithmatex_edge_cases.md │ ├── pymd_snippet.md │ ├── python_markdown_attr_list.md │ ├── regression.md │ ├── semantic_indent.md │ └── text.md ├── test_align_semantic_breaks_in_lists.py ├── test_format.py ├── test_ignore_missing_references.py ├── test_number.py ├── test_parsed_result.py ├── test_tabbed_code_block.py └── test_wrap.py ├── helpers.py ├── pre-commit-test-align_semantic_breaks_in_lists.md ├── pre-commit-test-ignore_missing_references.md ├── pre-commit-test-numbered.md ├── pre-commit-test-recommended.md ├── pre-commit-test.md ├── render ├── __init__.py ├── fixtures │ ├── material_admonitions.md │ ├── material_content_tabs.md │ ├── material_deflist.md │ ├── mkdocstrings_autorefs.md │ ├── mkdocstrings_crossreference.md │ ├── pymd_abbreviations.md │ ├── pymd_arithmatex.md │ ├── pymd_captions.md │ ├── pymd_snippet.md │ └── python_markdown_attr_list.md └── test_render.py └── test_mdformat.py /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # Answer file maintained by Copier for: https://github.com/KyleKing/mdformat-plugin-template 3 | # DO NOT MODIFY THIS FILE. Edit by re-running copier and changing responses to the questions 4 | # Check into version control. 5 | _commit: 2.6.0 6 | _src_path: gh:KyleKing/mdformat-plugin-template 7 | author_email: dev.act.kyle@gmail.com 8 | author_name: Kyle King 9 | author_username: kyleking 10 | copyright_date: '2024' 11 | package_name_kebab: mdformat-mkdocs 12 | plugin_name: mkdocs 13 | repository_namespace: kyleking 14 | repository_provider: https://github.com 15 | repository_url: https://github.com/kyleking/mdformat-mkdocs 16 | sync_admon_factories: true 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | "on": 5 | push: 6 | branches: [main] 7 | tags: [v*] 8 | pull_request: null 9 | 10 | jobs: 11 | prek: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v5 15 | - name: Set up Python 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: 3.14 19 | - uses: j178/prek-action@v1 20 | 21 | tests: 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | python-version: ["3.10", "3.14"] 26 | os: [ubuntu-latest, windows-latest] 27 | steps: 28 | - uses: actions/checkout@v5 29 | - name: Install uv and Python 30 | uses: astral-sh/setup-uv@v7 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | activate-environment: true 34 | - name: Install Package 35 | run: uv pip install ".[test]" 36 | - name: Run pytest 37 | run: pytest --cov 38 | 39 | prek-hook: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v5 43 | - name: Install uv and Python 44 | uses: astral-sh/setup-uv@v7 45 | with: 46 | python-version: 3.14 47 | activate-environment: true 48 | - name: Install prek 49 | uses: j178/prek-action@v1 50 | with: 51 | install-only: true 52 | - name: Install Package 53 | run: uv pip install ".[test]" 54 | - name: run prek with plugin 55 | run: prek run --config .pre-commit-test.yaml --all-files --verbose --show-diff-on-failure 56 | 57 | publish: 58 | name: Publish to PyPi 59 | needs: [prek, tests, prek-hook] 60 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 61 | runs-on: ubuntu-latest 62 | environment: 63 | name: pypi 64 | url: https://pypi.org/p/mdformat-mkdocs 65 | permissions: 66 | contents: write # For GitHub release creation 67 | id-token: write # IMPORTANT: mandatory for PyPI trusted publishing 68 | steps: 69 | - uses: actions/checkout@v5 70 | with: 71 | fetch-depth: 0 72 | fetch-tags: true 73 | - name: Install uv and Python 74 | uses: astral-sh/setup-uv@v7 75 | with: 76 | python-version: 3.14 77 | - name: Build Package 78 | run: uv build 79 | - name: Publish to PyPI 80 | uses: pypa/gh-action-pypi-publish@release/v1 81 | - name: Generate changelog with commitizen 82 | id: changelog 83 | run: | 84 | uv tool install commitizen 85 | VERSION="${GITHUB_REF_NAME#v}" 86 | CHANGELOG=$(cz changelog "$VERSION" --dry-run) 87 | { 88 | echo 'body<> "$GITHUB_OUTPUT" 92 | - name: Generate GitHub Release Notes 93 | uses: softprops/action-gh-release@v2 94 | with: 95 | body: ${{ steps.changelog.outputs.body }} 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Python (https://github.com/github/gitignore/blob/main/Python.gitignore) 3 | # ============================================================================== 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[codz] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | # Pipfile.lock 100 | 101 | # UV 102 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # uv.lock 106 | 107 | # poetry 108 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 109 | # This is especially recommended for binary packages to ensure reproducibility, and is more 110 | # commonly ignored for libraries. 111 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 112 | # poetry.lock 113 | # poetry.toml 114 | 115 | # pdm 116 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 117 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 118 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 119 | # pdm.lock 120 | # pdm.toml 121 | .pdm-python 122 | .pdm-build/ 123 | 124 | # pixi 125 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 126 | # pixi.lock 127 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 128 | # in the .venv directory. It is recommended not to include this directory in version control. 129 | .pixi 130 | 131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 132 | __pypackages__/ 133 | 134 | # Celery stuff 135 | celerybeat-schedule 136 | celerybeat.pid 137 | 138 | # Redis 139 | *.rdb 140 | *.aof 141 | *.pid 142 | 143 | # RabbitMQ 144 | mnesia/ 145 | rabbitmq/ 146 | rabbitmq-data/ 147 | 148 | # ActiveMQ 149 | activemq-data/ 150 | 151 | # SageMath parsed files 152 | *.sage.py 153 | 154 | # Environments 155 | .env 156 | .envrc 157 | .venv 158 | env/ 159 | venv/ 160 | ENV/ 161 | env.bak/ 162 | venv.bak/ 163 | 164 | # Spyder project settings 165 | .spyderproject 166 | .spyproject 167 | 168 | # Rope project settings 169 | .ropeproject 170 | 171 | # mkdocs documentation 172 | /site 173 | 174 | # mypy 175 | .mypy_cache/ 176 | .dmypy.json 177 | dmypy.json 178 | 179 | # Pyre type checker 180 | .pyre/ 181 | 182 | # pytype static type analyzer 183 | .pytype/ 184 | 185 | # Cython debug symbols 186 | cython_debug/ 187 | 188 | # PyCharm 189 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 190 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 191 | # and can be added to the global gitignore or merged into this file. For a more nuclear 192 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 193 | # .idea/ 194 | 195 | # Abstra 196 | # Abstra is an AI-powered process automation framework. 197 | # Ignore directories containing user credentials, local state, and settings. 198 | # Learn more at https://abstra.io/docs 199 | .abstra/ 200 | 201 | # Visual Studio Code 202 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 203 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 204 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 205 | # you could uncomment the following to ignore the entire vscode folder 206 | # .vscode/ 207 | 208 | # Ruff stuff: 209 | .ruff_cache/ 210 | 211 | # PyPI configuration file 212 | .pypirc 213 | 214 | # Marimo 215 | marimo/_static/ 216 | marimo/_lsp/ 217 | __marimo__/ 218 | 219 | # Streamlit 220 | .streamlit/secrets.toml 221 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v6.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-executables-have-shebangs 8 | - id: check-json 9 | - id: check-merge-conflict 10 | - id: check-symlinks 11 | - id: check-toml 12 | - id: check-vcs-permalinks 13 | - id: check-yaml 14 | args: [--unsafe] 15 | - id: debug-statements 16 | - id: destroyed-symlinks 17 | - id: detect-private-key 18 | - id: end-of-file-fixer 19 | exclude: \.copier-answers\.yml|__snapshots__/.*\.ambr 20 | - id: fix-byte-order-marker 21 | - id: forbid-new-submodules 22 | - id: mixed-line-ending 23 | args: [--fix=auto] 24 | - id: pretty-format-json 25 | args: [--autofix, --indent=4] 26 | - id: trailing-whitespace 27 | exclude: __snapshots__/.*\.ambr 28 | # - repo: https://github.com/executablebooks/mdformat 29 | # rev: 1.0.0 30 | # hooks: 31 | # - id: mdformat 32 | # additional_dependencies: 33 | # - mdformat-mkdocs[recommended-mdsf]>=5.1.0 34 | # - mdformat-gfm-alerts>=1.0.1 35 | # args: [--wrap=no] 36 | # exclude: tests/.+\.md 37 | # stages: ["pre-commit"] 38 | - repo: https://github.com/adrienverge/yamllint.git 39 | rev: v1.37.1 40 | hooks: 41 | - id: yamllint 42 | stages: ["pre-commit"] 43 | - repo: https://github.com/python-jsonschema/check-jsonschema 44 | rev: 0.35.0 45 | hooks: 46 | - id: check-github-workflows 47 | args: ["--verbose"] 48 | stages: ["pre-commit"] 49 | - repo: https://github.com/pappasam/toml-sort 50 | rev: v0.24.3 51 | hooks: 52 | - id: toml-sort-fix 53 | stages: ["pre-commit"] 54 | -------------------------------------------------------------------------------- /.pre-commit-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # A pre-commit hook for testing unreleased changes 3 | # Run from tox with: `tox -e hook-min` 4 | repos: 5 | - repo: local 6 | hooks: 7 | - id: mdformat 8 | name: mdformat-from-tox 9 | entry: mdformat 10 | files: tests/pre-commit-test.md 11 | types: [markdown] 12 | language: system 13 | - id: mdformat-with-args 14 | name: mdformat-with-args 15 | entry: mdformat 16 | args: [--wrap=40, --number] 17 | files: tests/pre-commit-test-numbered.md 18 | types: [markdown] 19 | language: system 20 | - id: mdformat-with-semantic-arg 21 | name: mdformat-with-semantic-arg 22 | entry: mdformat 23 | args: [--align-semantic-breaks-in-lists] 24 | files: tests/pre-commit-test-align_semantic_breaks_in_lists.md 25 | types: [markdown] 26 | language: system 27 | - id: mdformat-with-ignore-missing-references 28 | name: mdformat-with-ignore-missing-references 29 | entry: mdformat 30 | args: [--ignore-missing-references] 31 | files: tests/pre-commit-test-ignore_missing_references.md 32 | types: [markdown] 33 | language: system 34 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration 3 | extends: default 4 | 5 | rules: 6 | empty-values: enable 7 | float-values: enable 8 | line-length: disable 9 | octal-values: enable 10 | # quoted-strings: enable 11 | comments: 12 | min-spaces-from-content: 1 13 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS.md 2 | 3 | ## Testing 4 | 5 | ```bash 6 | # Run all tests using tox 7 | tox 8 | 9 | # Run tests with coverage (Python 3.14 - current version) 10 | tox -e test 11 | 12 | # Run tests with coverage (Python 3.10 - minimum version) 13 | tox -e test-min 14 | 15 | # Run specific tests with pytest flags 16 | tox -e test -- --exitfirst --failed-first --new-first -vv --snapshot-update 17 | ``` 18 | 19 | ## Linting and Formatting 20 | 21 | ```bash 22 | # Run all pre-commit hooks (using prek) 23 | tox -e prek 24 | # Or run directly with prek 25 | prek run --all 26 | 27 | # Run ruff for linting and formatting 28 | tox -e ruff 29 | # With unsafe fixes 30 | tox -e ruff -- --unsafe-fixes 31 | ``` 32 | 33 | ## Type Checking 34 | 35 | ```bash 36 | # Run mypy type checking 37 | tox -e type 38 | ``` 39 | 40 | ## Pre-commit Hook Testing 41 | 42 | ```bash 43 | # Test the plugin as a pre-commit hook 44 | tox -e hook-min 45 | ``` 46 | 47 | ## Architecture 48 | 49 | ### Plugin System 50 | 51 | The package implements mdformat's plugin interface with up to four key exports in `__init__.py`: 52 | 53 | - `update_mdit`: Registers markdown-it parser extensions 54 | - `add_cli_argument_group`: Optionally adds CLI flags 55 | - `RENDERERS`: Maps syntax tree node types to render functions 56 | - `POSTPROCESSORS`: Post-processes rendered output (list normalization, inline wrapping, deflist escaping) 57 | 58 | ### Core Components 59 | 60 | **mdformat_mkdocs/plugin.py** 61 | 62 | - Entry point that configures the mdformat plugin, registers all mdit_plugins, defines custom renders, and handles CLI configuration options 63 | 64 | **mdformat_mkdocs/\_normalize_list.py** 65 | 66 | - Complex list indentation normalization logic 67 | - Enforces 4-space indentation (MkDocs standard) instead of mdformat's default 2-space 68 | - Handles semantic line breaks with 3-space alignment for numbered lists when `--align-semantic-breaks-in-lists` is enabled 69 | - Parses list structure, code blocks, HTML blocks, and nested content 70 | - Uses functional programming patterns with `map_lookback` for stateful line processing 71 | 72 | **mdformat_mkdocs/mdit_plugins/** 73 | 74 | - Each file implements a markdown-it plugin for specific MkDocs/Python-Markdown syntax 75 | - Plugins parse syntax into tokens during the parsing phase 76 | - Corresponding renderers in `plugin.py` convert tokens back to formatted markdown 77 | 78 | **mdformat_mkdocs/\_helpers.py** 79 | 80 | - Shared utilities: `MKDOCS_INDENT_COUNT` (4 spaces), `separate_indent`, `get_conf` 81 | - Configuration reading from mdformat options (CLI, TOML, or API) 82 | 83 | **mdformat_mkdocs/\_synced/** 84 | 85 | - Contains code synced from other projects (admonition factories) 86 | - Check the README in these directories before modifying 87 | 88 | ### Configuration Options 89 | 90 | Configuration can be passed via: 91 | 92 | 1. Example CLI arguments: `--cli-argument` 93 | 1. Example TOML config file (`.mdformat.toml`): 94 | ```toml 95 | [plugin.mkdocs] 96 | cli_argument = true 97 | ``` 98 | 1. API: `mdformat.text(content, extensions={"mkdocs"}, options={...})` 99 | 100 | ### Testing Strategy 101 | 102 | **Snapshot Testing** 103 | 104 | - Uses `syrupy` for snapshot testing 105 | - Test fixtures in `tests/format/fixtures/` and `tests/render/fixtures/` 106 | - Main test file: `tests/test_mdformat.py` verifies idempotent formatting against `tests/pre-commit-test.md` 107 | 108 | **Test Organization** 109 | 110 | - `tests/format/`: Tests formatting output (input markdown → formatted markdown) 111 | - `tests/render/`: Tests HTML rendering (markdown → HTML via markdown-it) 112 | 113 | ## Development Notes 114 | 115 | - This project uses `uv-build` as the build backend 116 | - Uses `tox` for test automation with multiple Python versions (3.10, 3.14) 117 | - Pre-commit is configured but the project now uses `prek` (faster alternative) 118 | - Python 3.10+ is required (see `requires-python` in `pyproject.toml`) 119 | - Version is defined in `mdformat_mkdocs/__init__.py` as `__version__` 120 | 121 | ### Special Handling 122 | 123 | **List Indentation** 124 | 125 | - MkDocs requires 4-space indentation for nested list items 126 | - When `--align-semantic-breaks-in-lists` is enabled, continuation lines in ordered lists use 3-space indent (align with text after "1. ") 127 | - The `_normalize_list.py` module handles this complex logic with state machines tracking code blocks, HTML blocks, and list nesting 128 | 129 | **Link References** 130 | 131 | - By default, escapes undefined link references `[foo]` → `\[foo\]` 132 | - With `--ignore-missing-references`, leaves them as-is (required for mkdocstrings dynamic references) 133 | 134 | **Definition Lists** 135 | 136 | - Material for MkDocs definition lists require blank line between term and definition 137 | - Handled by `_material_deflist.py` plugin and special rendering in `plugin.py` 138 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | @AGENTS.md 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## API Documentation 4 | 5 | A collection of useful resources to reference when developing new features: 6 | 7 | - [`markdown-it-py` documentation](https://markdown-it-py.readthedocs.io/en/latest/using.html) 8 | - [`markdown-it` (JS) documentation](https://markdown-it.github.io/markdown-it) 9 | - [`mdformat` documentation](https://mdformat.readthedocs.io/en/stable) 10 | 11 | ## Local Development 12 | 13 | This package utilizes [mise](https://mise.jdx.dev) ([installation guide](https://github.com/jdx/mise/blob/79367a4d382d8ab4cb76afef357d0db4afa33866/docs/installing-mise.md)) for dependency management, [prek](https://github.com/j178/prek) for fast pre-commit hooks, [uv](https://docs.astral.sh/uv) as the build engine, and [tox](https://tox.readthedocs.io) for test automation. 14 | 15 | To install the development dependencies: 16 | 17 | ```bash 18 | brew install mise # or see the installation alternatives above 19 | 20 | # Install dependencies from mist.toml 21 | mise trust 22 | mise install 23 | 24 | # Configure prek 25 | prek install -f 26 | ``` 27 | 28 | To run all tox environments: 29 | 30 | ```bash 31 | tox 32 | ``` 33 | 34 | or to run specific commands: 35 | 36 | ```bash 37 | tox -e test 38 | tox -e prek 39 | tox -e hook-min 40 | 41 | tox list 42 | ``` 43 | 44 | To run all pre-commit steps: 45 | 46 | ```sh 47 | prek run --all 48 | ``` 49 | 50 | `pytest-watcher` is configured in `pyproject.toml` for `[tool.pytest-watcher]` to continuously run tests 51 | 52 | ```sh 53 | ptw . 54 | ``` 55 | 56 | ## Local uv/pipx integration testing 57 | 58 | Run the local code with `uv tool` (requires `uv` installed globally and first in `$PATH`, e.g. `brew install uv` or `mise use uv --global`) 59 | 60 | ```sh 61 | uv tool install 'mdformat>=0.7.19' --force --with=. 62 | 63 | # Then navigate to a different directory and check that the editable version was installed 64 | cd ~ 65 | mdformat --version 66 | which mdformat 67 | ``` 68 | 69 | Or with pipx: 70 | 71 | ```sh 72 | pipx install . --include-deps --force --editable 73 | ``` 74 | 75 | ## Publish to PyPI 76 | 77 | This project uses [PyPI Trusted Publishers](https://docs.pypi.org/trusted-publishers) for secure, token-free publishing from GitHub Actions, with [uv](https://docs.astral.sh/uv) for building packages. 78 | 79 | ### Initial Setup (One-time) 80 | 81 | Before publishing for the first time, you need to configure Trusted Publishing on PyPI: 82 | 83 | 1. Go to your project's page on PyPI: `https://pypi.org/manage/project/mdformat_mkdocs/settings/publishing/` 84 | - If the project doesn't exist yet, go to [PyPI's publishing page](https://pypi.org/manage/account/publishing) to add a "pending" publisher 85 | 1. Add a new Trusted Publisher with these settings: 86 | - **PyPI Project Name**: `mdformat_mkdocs` 87 | - **Owner**: `kyleking` 88 | - **Repository name**: `mdformat-mkdocs` 89 | - **Workflow name**: `tests.yml` (`.github/workflows/tests.yml`) 90 | - **Environment name**: `pypi` 91 | 1. Configure the GitHub Environment: 92 | - Go to your repository's `Settings` → `Environments` 93 | - Create an environment named `pypi` 94 | - (Recommended) Enable "Required reviewers" for production safety 95 | 96 | ### Publishing a Release 97 | 98 | Use `commitizen` to automatically bump versions (in `pyproject.toml` and `mdformat_mkdocs/__init__.py`) and create a commit with tag: 99 | 100 | ```sh 101 | # Dry run to preview the version bump 102 | tox -e cz -- --dry-run 103 | 104 | # Automatically bump version based on conventional commits 105 | tox -e cz 106 | 107 | # Or manually specify the increment type 108 | tox -e cz -- --increment PATCH # or MINOR or MAJOR 109 | 110 | # Push the commit and tag 111 | git push origin main --tags 112 | ``` 113 | 114 | The GitHub Action will automatically build and publish to PyPI using Trusted Publishers (no API tokens needed!). 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kyle King 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mdformat-mkdocs 2 | 3 | [![Build Status][ci-badge]][ci-link] [![PyPI version][pypi-badge]][pypi-link] 4 | 5 | An [mdformat](https://github.com/executablebooks/mdformat) plugin for [mkdocs](https://github.com/mkdocs/mkdocs) and packages commonly used with MkDocs ([mkdocs-material](https://squidfunk.github.io/mkdocs-material), [mkdocstrings](https://mkdocstrings.github.io), and [python-markdown](https://python-markdown.github.io)) 6 | 7 | Supports: 8 | 9 | - Indents are converted to four-spaces instead of two 10 | - *Note*: when specifying `--align-semantic-breaks-in-lists`, the nested indent for ordered lists is three, but is otherwise a multiple of four 11 | - Unordered list bullets are converted to dashes (`-`) instead of `*` 12 | - By default, ordered lists are standardized on a single digit (`1.` or `0.`) unless `--number` is specified, then `mdformat-mkdocs` will apply consecutive numbering to ordered lists [for consistency with `mdformat`](https://github.com/executablebooks/mdformat?tab=readme-ov-file#options) 13 | - [MkDocs-Material Admonitions\*](https://squidfunk.github.io/mkdocs-material/reference/admonitions) 14 | - \*Note: `mdformat-admon` will format the same admonitions, but for consistency with the mkdocs styleguide, an extra space will be added by this package ([#22](https://github.com/KyleKing/mdformat-admon/pull/22)) 15 | - [MkDocs-Material Content Tabs\*](https://squidfunk.github.io/mkdocs-material/reference/content-tabs) 16 | - \*Note: the markup (HTML) rendered by this plugin is sufficient for formatting but not for viewing in a browser. Please open an issue if you have a need to generate valid HTML. 17 | - [MkDocs-Material Definition Lists](https://squidfunk.github.io/mkdocs-material/reference/lists/#using-definition-lists) 18 | - [mkdocstrings Anchors (autorefs)](https://mkdocstrings.github.io/autorefs/#markdown-anchors) 19 | - [mkdocstrings Cross-References](https://mkdocstrings.github.io/usage/#cross-references) 20 | - [Python Markdown "Abbreviations"\*](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations) 21 | - \*Note: the markup (HTML) rendered for abbreviations is not useful for rendering. If important, I'm open to contributions because the implementation could be challenging 22 | - [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list) 23 | - Preserves attribute list syntax when using `--wrap` mode 24 | - [PyMdown Extensions "Arithmatex" (Math/LaTeX Support)](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex) ([Material for MkDocs Math](https://squidfunk.github.io/mkdocs-material/reference/math)) 25 | - This plugin combines three math rendering plugins from mdit-py-plugins: 26 | 1. **dollarmath**: Handles `$...$` (inline) and `$$...$$` (block) with smart dollar mode that prevents false positives (e.g., `$3.00` is not treated as math) 27 | 1. **texmath**: Handles `\(...\)` (inline) and `\[...\]` (block) LaTeX bracket notation 28 | 1. **amsmath**: Handles LaTeX environments like `\begin{align}...\end{align}`, `\begin{cases}...\end{cases}`, `\begin{matrix}...\end{matrix}`, etc. 29 | - Can be deactivated entirely with the `--no-mkdocs-math` flag 30 | - [Python Markdown "Snippets"\*](https://facelessuser.github.io/pymdown-extensions/extensions/snippets) 31 | - \*Note: the markup (HTML) renders the plain text without implementing the snippet logic. I'm open to contributions if anyone needs full support for snippets 32 | 33 | See the example test files, [./tests/pre-commit-test.md](https://raw.githubusercontent.com/KyleKing/mdformat-mkdocs/main/tests/pre-commit-test.md) and [./tests/format/fixtures.md](https://raw.githubusercontent.com/KyleKing/mdformat-mkdocs/main/tests/format/fixtures.md) 34 | 35 | ## `mdformat` Usage 36 | 37 | Add this package wherever you use `mdformat` and the plugin will be auto-recognized. No additional configuration necessary. For additional information on plugins, see [the official `mdformat` documentation here](https://mdformat.readthedocs.io/en/stable/users/plugins.html) 38 | 39 | ### Optional Extras 40 | 41 | This package specifies two optional "extra" plugins (`'recommended'` and `'recommended-mdsf'` ) for plugins that work well with typical documentation managed by `mkdocs`: 42 | 43 | - For `'recommended'`: 44 | - [mdformat-beautysh](https://pypi.org/project/mdformat-beautysh) 45 | - [mdformat-config](https://pypi.org/project/mdformat-config) 46 | - [mdformat-footnote](https://pypi.org/project/mdformat-footnote) 47 | - [mdformat-front-matters](https://pypi.org/project/mdformat-front-matters) (previously [mdformat-frontmatter](https://pypi.org/project/mdformat-frontmatter)) 48 | - [mdformat-gfm](https://github.com/hukkin/mdformat-gfm) 49 | - [mdformat-ruff](https://github.com/Freed-Wu/mdformat-ruff) 50 | - [mdformat-simple-breaks](https://pypi.org/project/mdformat-simple-breaks) 51 | - [mdformat-web](https://pypi.org/project/mdformat-web) 52 | - [mdformat-wikilink](https://github.com/tmr232/mdformat-wikilink) 53 | - For `'recommended-mdsf'`: 54 | - Instead of `mdformat-beautysh`, `mdformat-config`, `mdformat-ruff`, and `mdformat-web`, the "mdsf" extras install `mdformat-hooks`, which allows the use of `mdsf` for formatting code blocks in hundreds of languages using CLI formatters you already have installed; however, this requires extra configuration, so make sure to see the README: 55 | 56 | ### pre-commit/prek 57 | 58 | ```yaml 59 | repos: 60 | - repo: https://github.com/executablebooks/mdformat 61 | rev: 1.0.0 62 | hooks: 63 | - id: mdformat 64 | additional_dependencies: 65 | - mdformat-mkdocs 66 | # Or 67 | # - "mdformat-mkdocs[recommended-mdsf]>=5.1.0" 68 | # Or 69 | # - "mdformat-mkdocs[recommended]" 70 | ``` 71 | 72 | ### uvx 73 | 74 | ```sh 75 | uvx --with mdformat-mkdocs mdformat 76 | ``` 77 | 78 | Or with pipx: 79 | 80 | ```sh 81 | pipx install mdformat 82 | pipx inject mdformat mdformat-mkdocs 83 | ``` 84 | 85 | ## HTML Rendering 86 | 87 | To generate HTML output, any of the plugins can be imported from `mdit_plugins`. For more guidance on `MarkdownIt`, see the docs: 88 | 89 | ```py 90 | from markdown_it import MarkdownIt 91 | 92 | from mdformat_mkdocs.mdit_plugins import ( 93 | material_admon_plugin, 94 | material_content_tabs_plugin, 95 | mkdocstrings_autorefs_plugin, 96 | mkdocstrings_crossreference_plugin, 97 | pymd_abbreviations_plugin, 98 | ) 99 | 100 | md = MarkdownIt() 101 | md.use(material_admon_plugin) 102 | md.use(material_content_tabs_plugin) 103 | md.use(mkdocstrings_autorefs_plugin) 104 | md.use(mkdocstrings_crossreference_plugin) 105 | md.use(pymd_abbreviations_plugin) 106 | 107 | text = "- Line 1\n - `bash command`\n - Line 3" 108 | md.render(text) 109 | #
    110 | #
  • Line 1 111 | #
      112 | #
    • bash command
    • 113 | #
    • Line 3
    • 114 | #
    115 | #
  • 116 | #
117 | ``` 118 | 119 | ## Configuration 120 | 121 | `mdformat-mkdocs` adds the CLI arguments: 122 | 123 | - `--align-semantic-breaks-in-lists` to optionally align line breaks in numbered lists to 3-spaces. If not specified, the default of 4-indents is followed universally. 124 | 125 | ```txt 126 | # with: mdformat 127 | 1. Semantic line feed where the following line is 128 | three spaces deep 129 | 130 | # vs. "mdformat --align-semantic-breaks-in-lists" 131 | 1. Semantic line feed where the following line is 132 | three spaces deep 133 | ``` 134 | 135 | - `--ignore-missing-references` if set, do not escape link references when no definition is found. This is required when references are dynamic, such as with python mkdocstrings 136 | 137 | - `--no-mkdocs-math` if set, deactivate math/LaTeX rendering (Arithmatex). By default, math is enabled. This can be useful if you want to format markdown without processing math syntax. 138 | 139 | You can also use the toml configuration (https://mdformat.readthedocs.io/en/stable/users/configuration_file.html): 140 | 141 | ```toml 142 | # .mdformat.toml 143 | 144 | [plugin.mkdocs] 145 | align_semantic_breaks_in_lists = true 146 | ignore_missing_references = true 147 | no_mkdocs_math = true 148 | ``` 149 | 150 | ## Contributing 151 | 152 | See [CONTRIBUTING.md](https://github.com/kyleking/mdformat-mkdocs/blob/main/CONTRIBUTING.md) 153 | 154 | [ci-badge]: https://github.com/kyleking/mdformat-mkdocs/actions/workflows/tests.yml/badge.svg?branch=main 155 | [ci-link]: https://github.com/kyleking/mdformat-mkdocs/actions?query=workflow%3ACI+branch%3Amain+event%3Apush 156 | [pypi-badge]: https://img.shields.io/pypi/v/mdformat-mkdocs.svg 157 | [pypi-link]: https://pypi.org/project/mdformat-mkdocs 158 | -------------------------------------------------------------------------------- /mdformat_mkdocs/__init__.py: -------------------------------------------------------------------------------- 1 | """An mdformat plugin for `mkdocs`.""" 2 | 3 | __version__ = "5.1.1" 4 | 5 | __plugin_name__ = "mkdocs" 6 | 7 | # FYI see source code for available interfaces: 8 | # https://github.com/executablebooks/mdformat/blob/5d9b573ce33bae219087984dd148894c774f41d4/src/mdformat/plugins.py 9 | from .plugin import POSTPROCESSORS, RENDERERS, add_cli_argument_group, update_mdit 10 | 11 | __all__ = ("POSTPROCESSORS", "RENDERERS", "add_cli_argument_group", "update_mdit") 12 | -------------------------------------------------------------------------------- /mdformat_mkdocs/_helpers.py: -------------------------------------------------------------------------------- 1 | """General Helpers.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | from collections.abc import Callable, Mapping 7 | from functools import wraps 8 | from typing import Any 9 | 10 | from . import __plugin_name__ 11 | 12 | EOL = "\n" 13 | """Line delimiter.""" 14 | 15 | MKDOCS_INDENT_COUNT = 4 16 | """Use 4-spaces for mkdocs.""" 17 | 18 | FILLER_CHAR = "𝕏" # noqa: RUF001 19 | """A spacer that is inserted and then removed to ensure proper word wrap.""" 20 | 21 | 22 | def rstrip_result(func: Callable[..., str]) -> Callable[..., str]: 23 | """Right-strip the decorated function's result. 24 | 25 | Returns: 26 | Callable[..., str]: decorator 27 | 28 | """ 29 | 30 | @wraps(func) 31 | def _wrapper(*args, **kwargs) -> str: 32 | return func(*args, **kwargs).rstrip() 33 | 34 | return _wrapper 35 | 36 | 37 | def separate_indent(line: str) -> tuple[str, str]: 38 | """Separate leading indent from content. Also used by the test suite. 39 | 40 | Returns: 41 | tuple[str, str]: separate indent and content 42 | 43 | """ 44 | re_indent = re.compile(r"(?P\s*)(?P[^\s]?.*)") 45 | match = re_indent.match(line) 46 | assert match # for pyright 47 | return (match["indent"], match["content"]) 48 | 49 | 50 | ContextOptions = Mapping[str, Any] 51 | 52 | 53 | def get_conf(options: ContextOptions, key: str) -> bool | str | int | None: 54 | """Read setting from mdformat configuration Context.""" 55 | if (api := options["mdformat"].get(key)) is not None: 56 | return api # From API 57 | return ( 58 | options["mdformat"].get("plugin", {}).get(__plugin_name__, {}).get(key) 59 | ) # from cli_or_toml 60 | -------------------------------------------------------------------------------- /mdformat_mkdocs/_postprocess_inline.py: -------------------------------------------------------------------------------- 1 | """Postprocess inline.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from mdformat.renderer import WRAP_POINT 8 | 9 | from ._helpers import FILLER_CHAR, MKDOCS_INDENT_COUNT, get_conf, rstrip_result 10 | 11 | if TYPE_CHECKING: 12 | from mdformat.renderer import RenderContext, RenderTreeNode 13 | 14 | FILLER = FILLER_CHAR * (MKDOCS_INDENT_COUNT - 2) # `mdformat` default is two spaces 15 | """A spacer that is inserted and then removed to ensure proper word wrap.""" 16 | 17 | 18 | @rstrip_result 19 | def postprocess_list_wrap( 20 | text: str, 21 | node: RenderTreeNode, 22 | context: RenderContext, 23 | ) -> str: 24 | """Postprocess inline tokens. 25 | 26 | Fix word wrap for lists to account for the change in indentation. 27 | We fool word wrap by prefixing an unwrappable dummy string of the same length. 28 | This prefix needs to be later removed (in `merge_parsed_text`). 29 | 30 | Adapted from: 31 | https://github.com/hukkin/mdformat-gfm/blob/cf316a121b6cf35cbff7b0ad6e171f287803f8cb/src/mdformat_gfm/plugin.py#L86-L111 32 | 33 | """ 34 | if not context.do_wrap: 35 | return text 36 | wrap_mode = get_conf(context.options, "wrap") 37 | if ( 38 | not isinstance(wrap_mode, int) # noqa: PLR0916 39 | or FILLER_CHAR in text 40 | or (node.parent and node.parent.type != "paragraph") 41 | or ( 42 | node.parent 43 | and node.parent.parent 44 | and node.parent.parent.type != "list_item" 45 | ) 46 | ): 47 | return text 48 | 49 | counter_ = -1 50 | parent = node.parent 51 | while parent and parent.type == "paragraph": 52 | parent = parent.parent 53 | counter_ += 1 54 | indent_count = max(counter_, 0) 55 | 56 | text = text.lstrip(WRAP_POINT).lstrip() 57 | filler = (FILLER * indent_count)[:-1] if indent_count else "" 58 | newline_filler = filler + FILLER if indent_count else FILLER[:-1] 59 | if len(text) > wrap_mode: 60 | indent_length = MKDOCS_INDENT_COUNT * indent_count 61 | wrapped_length = -1 * wrap_mode 62 | words: list[str] = [] 63 | split = text.split(WRAP_POINT) 64 | for idx, word in enumerate(split): 65 | next_length = wrapped_length + len(word) 66 | if not words: 67 | words = [filler, word] 68 | wrapped_length = indent_length + len(word) 69 | elif next_length > wrap_mode: 70 | if idx < len(split) - 1: 71 | words += [word, newline_filler] 72 | wrapped_length = indent_length + len(word) 73 | else: 74 | words.append(word) 75 | wrapped_length = len(word) 76 | else: 77 | words.append(word) 78 | wrapped_length = next_length + 1 79 | return WRAP_POINT.join(_w for _w in words if _w) 80 | return f"{filler}{WRAP_POINT}{text}" if filler else text 81 | -------------------------------------------------------------------------------- /mdformat_mkdocs/_synced/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdformat_mkdocs/_synced/admon_factories/README.md: -------------------------------------------------------------------------------- 1 | # Admonition/Callout Factories 2 | 3 | This code is useful to format and render admonitions similar to Python Markdown's format 4 | 5 | If you are looking to add `mdformat` to your project to format a specific syntax, you will want to use one of the below plugins: 6 | 7 | - [`mdformat-admon`](https://github.com/KyleKing/mdformat-admon) 8 | - [`python-markdown` admonitions](https://python-markdown.github.io/extensions/admonition) 9 | - [`mdformat-mkdocs`](https://github.com/KyleKing/mdformat-mkdocs) 10 | - [MKDocs Admonitions](https://squidfunk.github.io/mkdocs-material/reference/admonitions) 11 | - [`mdformat-gfm-alerts`](https://github.com/KyleKing/mdformat-gfm-alerts) 12 | - Primarily supports [Github "Alerts"](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts), but indirectly also supports 13 | - [Microsoft "Alerts"](https://learn.microsoft.com/en-us/contribute/content/markdown-reference#alerts-note-tip-important-caution-warning) 14 | - [Mozilla Callouts](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/Howto/Markdown_in_MDN#notes_warnings_and_callouts) 15 | - [`mdformat-obsidian`](https://github.com/KyleKing/mdformat-obsidian) 16 | - [Obsidian Callouts](https://help.obsidian.md/How+to/Use+callouts) 17 | 18 | However, directive-style admonition formats are not known to be supported by an existing mdformat plugin nor by the utility code in this directory as it exists today: 19 | 20 | - [node.js markdown-it-container](https://github.com/markdown-it/markdown-it-container) 21 | - [MyST](https://myst-parser.readthedocs.io/en/latest/syntax/roles-and-directives.html) 22 | - [Sphinx Directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html) 23 | - [reStructuredText](https://docutils.sourceforge.io/docs/ref/rst/directives.html#specific-admonitions) 24 | - [pymdown-extensions](https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/admonition) 25 | - [PyMDown](https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/admonition) 26 | -------------------------------------------------------------------------------- /mdformat_mkdocs/_synced/admon_factories/__init__.py: -------------------------------------------------------------------------------- 1 | from ._whitespace_admon_factories import ( 2 | AdmonitionData, 3 | admon_plugin_factory, 4 | new_token, 5 | parse_possible_whitespace_admon_factory, 6 | parse_tag_and_title, 7 | ) 8 | 9 | __all__ = ( 10 | "AdmonitionData", 11 | "admon_plugin_factory", 12 | "new_token", 13 | "parse_possible_whitespace_admon_factory", 14 | "parse_tag_and_title", 15 | ) 16 | -------------------------------------------------------------------------------- /mdformat_mkdocs/_synced/admon_factories/_whitespace_admon_factories.py: -------------------------------------------------------------------------------- 1 | """Note, this is ported from `markdown-it-admon` .""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | from collections.abc import Callable, Generator, Sequence 7 | from contextlib import contextmanager, suppress 8 | from typing import NamedTuple 9 | 10 | from markdown_it import MarkdownIt 11 | from markdown_it.renderer import RendererProtocol 12 | from markdown_it.ruler import RuleOptionsType 13 | from markdown_it.rules_block import StateBlock 14 | from markdown_it.rules_inline import StateInline 15 | from markdown_it.token import Token 16 | from markdown_it.utils import EnvType, OptionsDict 17 | from mdit_py_plugins.utils import is_code_block 18 | 19 | 20 | def _get_multiple_tags(meta_text: str) -> tuple[list[str], str]: 21 | """Check for multiple tags when the title is double quoted. 22 | 23 | Raises: 24 | ValueError: if no tags matched 25 | 26 | """ 27 | re_tags = re.compile(r'^\s*(?P[^"]+)\s+"(?P.*)"\S*$') 28 | if match := re_tags.match(meta_text): 29 | tags = match["tokens"].strip().split(" ") 30 | return [tag.lower() for tag in tags], match["title"] 31 | raise ValueError("No match found for parameters") 32 | 33 | 34 | def parse_tag_and_title(admon_meta_text: str) -> tuple[list[str], str]: 35 | """Separate the tag name from the admonition title.""" 36 | if not (meta_text := admon_meta_text.strip()): 37 | return [""], "" 38 | 39 | with suppress(ValueError): 40 | return _get_multiple_tags(meta_text) 41 | 42 | tag, *title_ = meta_text.split(" ") 43 | joined = " ".join(title_) 44 | 45 | title = "" 46 | if not joined: 47 | title = tag.title() 48 | elif joined != '""': # Specifically check for no title 49 | title = joined 50 | return [tag.lower()], title 51 | 52 | 53 | def validate_admon_meta(meta_text: str) -> bool: 54 | """Validate the presence of the tag name after the marker.""" 55 | tag = meta_text.strip().split(" ", 1)[-1] or "" 56 | return bool(tag) 57 | 58 | 59 | class AdmonState(NamedTuple): 60 | """Frozen state using the same variable case.""" 61 | 62 | parentType: str 63 | lineMax: int 64 | blkIndent: int 65 | 66 | 67 | class AdmonitionData(NamedTuple): 68 | """AdmonitionData data for rendering.""" 69 | 70 | old_state: AdmonState 71 | marker: str 72 | markup: str 73 | meta_text: str 74 | next_line: int 75 | 76 | 77 | def search_admon_end(state: StateBlock, start_line: int, end_line: int) -> int: 78 | was_empty = False 79 | 80 | # Search for the end of the block 81 | next_line = start_line 82 | is_fenced = False 83 | while True: 84 | next_line += 1 85 | if next_line >= end_line: 86 | # unclosed block should be autoclosed by end of document. 87 | # also block seems to be autoclosed by end of parent 88 | break 89 | pos = state.bMarks[next_line] + state.tShift[next_line] 90 | maximum = state.eMarks[next_line] 91 | is_empty = state.sCount[next_line] < state.blkIndent 92 | 93 | # two consecutive empty lines autoclose the block, unless the block is fenced 94 | if not is_fenced and is_empty and was_empty: 95 | break 96 | was_empty = is_empty 97 | 98 | # Check if line starts with ``` 99 | if state.src[pos : pos + 3] == "```": 100 | is_fenced = not is_fenced 101 | 102 | if pos < maximum and state.sCount[next_line] < state.blkIndent: 103 | # non-empty line with negative indent should stop the block: 104 | # - !!! 105 | # test 106 | break 107 | 108 | return next_line 109 | 110 | 111 | def parse_possible_whitespace_admon_factory( 112 | markers: set[str], 113 | ) -> Callable[[StateBlock, int, int, bool], AdmonitionData | bool]: 114 | expected_marker_len = 3 # Regardless of extra chars, block indent stays the same 115 | marker_first_chars = {_m[0] for _m in markers} 116 | max_marker_len = max(len(_m) for _m in markers) 117 | 118 | def parse_possible_whitespace_admon( 119 | state: StateBlock, 120 | start_line: int, 121 | end_line: int, 122 | silent: bool, 123 | ) -> AdmonitionData | bool: 124 | if is_code_block(state, start_line): 125 | return False 126 | 127 | start = state.bMarks[start_line] + state.tShift[start_line] 128 | maximum = state.eMarks[start_line] 129 | 130 | # Exit quickly on a non-match for first char 131 | if state.src[start] not in marker_first_chars: 132 | return False 133 | 134 | # Check out the rest of the marker string 135 | marker = "" 136 | marker_len = max_marker_len 137 | marker_pos = 0 138 | markup = "" 139 | while marker_len > 0: 140 | marker_pos = start + marker_len 141 | if (markup := state.src[start:marker_pos]) in markers: 142 | marker = markup 143 | break 144 | marker_len -= 1 145 | else: 146 | return False 147 | 148 | admon_meta_text = state.src[marker_pos:maximum] 149 | if not validate_admon_meta(admon_meta_text): 150 | return False 151 | # Since start is found, we can report success here in validation mode 152 | if silent: 153 | return True 154 | 155 | old_state = AdmonState( 156 | parentType=state.parentType, 157 | lineMax=state.lineMax, 158 | blkIndent=state.blkIndent, 159 | ) 160 | state.parentType = "admonition" 161 | 162 | blk_start = marker_pos 163 | while blk_start < maximum and state.src[blk_start] == " ": 164 | blk_start += 1 165 | 166 | # Correct block indentation when extra marker characters are present 167 | marker_alignment_correction = expected_marker_len - len(marker) 168 | state.blkIndent += blk_start - start + marker_alignment_correction 169 | 170 | next_line = search_admon_end(state, start_line, end_line) 171 | 172 | # this will prevent lazy continuations from ever going past our end marker 173 | state.lineMax = next_line 174 | return AdmonitionData( 175 | old_state=old_state, 176 | marker=marker, 177 | markup=markup, 178 | meta_text=admon_meta_text, 179 | next_line=next_line, 180 | ) 181 | 182 | return parse_possible_whitespace_admon 183 | 184 | 185 | @contextmanager 186 | def new_token( 187 | state: StateBlock | StateInline, 188 | name: str, 189 | kind: str, 190 | ) -> Generator[Token, None, None]: 191 | """Create scoped token.""" 192 | yield state.push(f"{name}_open", kind, 1) 193 | state.push(f"{name}_close", kind, -1) 194 | 195 | 196 | def default_render( 197 | self: RendererProtocol, 198 | tokens: Sequence[Token], 199 | idx: int, 200 | _options: OptionsDict, 201 | env: EnvType, 202 | ) -> str: 203 | """Render token if no more specific renderer is specified.""" 204 | return self.renderToken(tokens, idx, _options, env) # type: ignore[attr-defined] 205 | 206 | 207 | RenderType = Callable[..., str] 208 | 209 | 210 | def admon_plugin_factory( 211 | prefix: str, 212 | logic: Callable[[StateBlock, int, int, bool], bool], 213 | ) -> Callable[[MarkdownIt, RenderType | None], None]: 214 | def admon_plugin(md: MarkdownIt, render: RenderType | None = None) -> None: 215 | render = render or default_render 216 | 217 | md.add_render_rule(f"{prefix}_open", render) 218 | md.add_render_rule(f"{prefix}_close", render) 219 | md.add_render_rule(f"{prefix}_title_open", render) 220 | md.add_render_rule(f"{prefix}_title_close", render) 221 | 222 | options: RuleOptionsType = { 223 | "alt": ["paragraph", "reference", "blockquote", "list"], 224 | } 225 | md.block.ruler.before("fence", prefix, logic, options) 226 | 227 | return admon_plugin 228 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """Plugins.""" 2 | 3 | from ._material_admon import MATERIAL_ADMON_MARKERS, material_admon_plugin 4 | from ._material_content_tabs import ( 5 | MATERIAL_CONTENT_TAB_MARKERS, 6 | material_content_tabs_plugin, 7 | ) 8 | from ._material_deflist import ( 9 | escape_deflist, 10 | material_deflist_plugin, 11 | render_material_definition_body, 12 | render_material_definition_list, 13 | render_material_definition_term, 14 | ) 15 | from ._mkdocstrings_autorefs import ( 16 | MKDOCSTRINGS_AUTOREFS_PREFIX, 17 | MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX, 18 | mkdocstrings_autorefs_plugin, 19 | ) 20 | from ._mkdocstrings_crossreference import ( 21 | MKDOCSTRINGS_CROSSREFERENCE_PREFIX, 22 | mkdocstrings_crossreference_plugin, 23 | ) 24 | from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin 25 | from ._pymd_admon import pymd_admon_plugin 26 | from ._pymd_arithmatex import ( 27 | AMSMATH_BLOCK, 28 | DOLLARMATH_BLOCK, 29 | DOLLARMATH_BLOCK_LABEL, 30 | DOLLARMATH_INLINE, 31 | TEXMATH_BLOCK_EQNO, 32 | pymd_arithmatex_plugin, 33 | ) 34 | from ._pymd_captions import PYMD_CAPTIONS_PREFIX, pymd_captions_plugin 35 | from ._pymd_snippet import PYMD_SNIPPET_PREFIX, pymd_snippet_plugin 36 | from ._python_markdown_attr_list import ( 37 | PYTHON_MARKDOWN_ATTR_LIST_PREFIX, 38 | python_markdown_attr_list_plugin, 39 | ) 40 | 41 | __all__ = ( 42 | "AMSMATH_BLOCK", 43 | "DOLLARMATH_BLOCK", 44 | "DOLLARMATH_BLOCK_LABEL", 45 | "DOLLARMATH_INLINE", 46 | "MATERIAL_ADMON_MARKERS", 47 | "MATERIAL_CONTENT_TAB_MARKERS", 48 | "MKDOCSTRINGS_AUTOREFS_PREFIX", 49 | "MKDOCSTRINGS_CROSSREFERENCE_PREFIX", 50 | "MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX", 51 | "PYMD_ABBREVIATIONS_PREFIX", 52 | "PYMD_CAPTIONS_PREFIX", 53 | "PYMD_SNIPPET_PREFIX", 54 | "PYTHON_MARKDOWN_ATTR_LIST_PREFIX", 55 | "TEXMATH_BLOCK_EQNO", 56 | "escape_deflist", 57 | "material_admon_plugin", 58 | "material_content_tabs_plugin", 59 | "material_deflist_plugin", 60 | "mkdocstrings_autorefs_plugin", 61 | "mkdocstrings_crossreference_plugin", 62 | "pymd_abbreviations_plugin", 63 | "pymd_admon_plugin", 64 | "pymd_arithmatex_plugin", 65 | "pymd_captions_plugin", 66 | "pymd_snippet_plugin", 67 | "python_markdown_attr_list_plugin", 68 | "render_material_definition_body", 69 | "render_material_definition_list", 70 | "render_material_definition_term", 71 | ) 72 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_material_admon.py: -------------------------------------------------------------------------------- 1 | """Match `mkdocs-material` admonitions. 2 | 3 | Matches: 4 | 5 | ```md 6 | !!! note 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod 9 | nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor 10 | massa, nec semper lorem quam in massa. 11 | ``` 12 | 13 | Docs: <https://squidfunk.github.io/mkdocs-material/reference/admonitions/> 14 | 15 | """ 16 | 17 | from __future__ import annotations 18 | 19 | from typing import TYPE_CHECKING, Any 20 | 21 | from mdformat_mkdocs._synced.admon_factories import ( 22 | AdmonitionData, 23 | admon_plugin_factory, 24 | new_token, 25 | parse_possible_whitespace_admon_factory, 26 | parse_tag_and_title, 27 | ) 28 | 29 | from ._pymd_admon import format_pymd_admon_markup 30 | 31 | if TYPE_CHECKING: 32 | from markdown_it.rules_block import StateBlock 33 | 34 | MATERIAL_ADMON_PREFIX = "admonition_mkdocs" 35 | """Prefix used to differentiate the parsed output.""" 36 | 37 | MATERIAL_ADMON_MARKERS = {"!!!", "???", "???+"} 38 | """All supported MkDocs Admonition markers.""" 39 | 40 | 41 | def format_admon_markup( 42 | state: StateBlock, 43 | start_line: int, 44 | admonition: AdmonitionData, 45 | ) -> None: 46 | """Format markup.""" 47 | if admonition.marker == "!!!": 48 | format_pymd_admon_markup(state, start_line, admonition) 49 | return 50 | 51 | tags, title = parse_tag_and_title(admonition.meta_text) 52 | tag = tags[0] 53 | 54 | with new_token(state, MATERIAL_ADMON_PREFIX, "details") as token: 55 | token.markup = admonition.markup 56 | token.block = True 57 | attrs: dict[str, Any] = {"class": " ".join(tags)} 58 | if admonition.markup.endswith("+"): 59 | attrs["open"] = "open" 60 | token.attrs = attrs 61 | token.meta = {"tag": tag} 62 | token.info = admonition.meta_text 63 | token.map = [start_line, admonition.next_line] 64 | 65 | if title: 66 | title_markup = f"{admonition.markup} {tag}" 67 | with new_token( 68 | state, 69 | f"{MATERIAL_ADMON_PREFIX}_title", 70 | "summary", 71 | ) as tkn_title: 72 | tkn_title.markup = title_markup 73 | tkn_title.map = [start_line, start_line + 1] 74 | 75 | tkn_inline = state.push("inline", "", 0) 76 | tkn_inline.content = title 77 | tkn_inline.map = [start_line, start_line + 1] 78 | tkn_inline.children = [] 79 | 80 | state.md.block.tokenize(state, start_line + 1, admonition.next_line) 81 | 82 | state.parentType = admonition.old_state.parentType 83 | state.lineMax = admonition.old_state.lineMax 84 | state.blkIndent = admonition.old_state.blkIndent 85 | state.line = admonition.next_line 86 | 87 | 88 | def admonition_logic( 89 | state: StateBlock, 90 | start_line: int, 91 | end_line: int, 92 | silent: bool, 93 | ) -> bool: 94 | """Parse MkDocs-style Admonitions. 95 | 96 | `Such as collapsible blocks 97 | <https://squidfunk.github.io/mkdocs-material/reference/admonitions/#collapsible-blocks>`. 98 | 99 | .. code-block:: md 100 | 101 | ???+ note 102 | *content* 103 | 104 | """ 105 | parse_possible_whitespace_admon = parse_possible_whitespace_admon_factory( 106 | markers=MATERIAL_ADMON_MARKERS, 107 | ) 108 | result = parse_possible_whitespace_admon(state, start_line, end_line, silent) 109 | if isinstance(result, AdmonitionData): 110 | format_admon_markup(state, start_line, admonition=result) 111 | return True 112 | return result 113 | 114 | 115 | material_admon_plugin = admon_plugin_factory(MATERIAL_ADMON_PREFIX, admonition_logic) 116 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_material_content_tabs.py: -------------------------------------------------------------------------------- 1 | """Match `mkdocs-material` Content Tabs. 2 | 3 | Matches: 4 | 5 | ```md 6 | === "C" 7 | 8 | ``` c 9 | #include <stdio.h> 10 | 11 | int main(void) { 12 | printf("Hello world!"); 13 | return 0; 14 | } 15 | ``` 16 | 17 | === "C++" 18 | 19 | ``` c++ 20 | #include <iostream> 21 | 22 | int main(void) { 23 | std::cout << "Hello world!" << std::endl; 24 | return 0; 25 | } 26 | ``` 27 | ``` 28 | 29 | Docs: <https://squidfunk.github.io/mkdocs-material/reference/content-tabs> 30 | 31 | """ 32 | 33 | from markdown_it.rules_block import StateBlock 34 | 35 | from mdformat_mkdocs._synced.admon_factories import ( 36 | AdmonitionData, 37 | admon_plugin_factory, 38 | new_token, 39 | parse_possible_whitespace_admon_factory, 40 | ) 41 | 42 | MATERIAL_CONTENT_TAB_PREFIX = "content_tab_mkdocs" 43 | """Prefix used to differentiate the parsed output.""" 44 | 45 | MATERIAL_CONTENT_TAB_MARKERS = {"===", "===!", "===+"} 46 | """All supported content tab markers.""" 47 | 48 | 49 | def format_content_tab_markup( 50 | state: StateBlock, 51 | start_line: int, 52 | admonition: AdmonitionData, 53 | ) -> None: 54 | """WARNING: this is not the proper markup for MkDocs. 55 | 56 | Would require recursively calling the parser to identify all sequential 57 | content tabs 58 | 59 | """ 60 | title = admonition.meta_text.strip().strip("'\"") 61 | 62 | with new_token(state, MATERIAL_CONTENT_TAB_PREFIX, "div") as token: 63 | token.markup = admonition.markup 64 | token.block = True 65 | token.attrs = {"class": "content-tab"} 66 | token.info = admonition.meta_text 67 | token.map = [start_line, admonition.next_line] 68 | 69 | with new_token(state, f"{MATERIAL_CONTENT_TAB_PREFIX}_title", "p") as tkn_inner: 70 | tkn_inner.attrs = {"class": "content-tab-title"} 71 | tkn_inner.map = [start_line, start_line + 1] 72 | 73 | tkn_inline = state.push("inline", "", 0) 74 | tkn_inline.content = title 75 | tkn_inline.map = [start_line, start_line + 1] 76 | tkn_inline.children = [] 77 | 78 | state.md.block.tokenize(state, start_line + 1, admonition.next_line) 79 | 80 | state.parentType = admonition.old_state.parentType 81 | state.lineMax = admonition.old_state.lineMax 82 | state.blkIndent = admonition.old_state.blkIndent 83 | state.line = admonition.next_line 84 | 85 | 86 | def content_tab_logic( 87 | state: StateBlock, 88 | start_line: int, 89 | end_line: int, 90 | silent: bool, 91 | ) -> bool: 92 | # Because content-tabs look like admonitions syntactically, we can 93 | # reuse admonition parsing logic 94 | # Supported variations from: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed 95 | parse_possible_whitespace_admon = parse_possible_whitespace_admon_factory( 96 | markers=MATERIAL_CONTENT_TAB_MARKERS, 97 | ) 98 | result = parse_possible_whitespace_admon(state, start_line, end_line, silent) 99 | if isinstance(result, AdmonitionData): 100 | format_content_tab_markup(state, start_line, admonition=result) 101 | return True 102 | return result 103 | 104 | 105 | material_content_tabs_plugin = admon_plugin_factory( 106 | MATERIAL_CONTENT_TAB_PREFIX, 107 | content_tab_logic, 108 | ) 109 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_material_deflist.py: -------------------------------------------------------------------------------- 1 | """Material Definition Lists. 2 | 3 | Based on 4 | [mdformat-deflist](https://github.com/executablebooks/mdformat-deflist/blob/bbcf9ed4f80847db58b6f932ed95e2c7a6c49ae5/mdformat_deflist/plugin.py), 5 | but modified for mkdocs-material conventions. 6 | 7 | Example: 8 | ```md 9 | `Lorem ipsum dolor sit amet` 10 | 11 | : Sed sagittis eleifend rutrum. Donec vitae suscipit est. Nullam tempus 12 | tellus non sem sollicitudin, quis rutrum leo facilisis. 13 | 14 | `Cras arcu libero` 15 | 16 | : Aliquam metus eros, pretium sed nulla venenatis, faucibus auctor ex. Proin 17 | ut eros sed sapien ullamcorper consequat. Nunc ligula ante. 18 | 19 | Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis. 20 | Vulputate tincidunt fringilla. 21 | Nullam dignissim ultrices urna non auctor. 22 | ``` 23 | 24 | Docs: 25 | <https://squidfunk.github.io/mkdocs-material/reference/lists/#using-definition-lists> 26 | 27 | """ 28 | 29 | from __future__ import annotations 30 | 31 | import re 32 | from typing import TYPE_CHECKING 33 | 34 | from markdown_it import MarkdownIt 35 | from mdit_py_plugins.deflist import deflist_plugin 36 | 37 | if TYPE_CHECKING: 38 | from markdown_it import MarkdownIt 39 | from mdformat.renderer import RenderContext, RenderTreeNode 40 | from mdformat.renderer.typing import Render 41 | 42 | 43 | def material_deflist_plugin(md: MarkdownIt) -> None: 44 | """Add mkdocs-material definition list support to markdown-it parser.""" 45 | md.use(deflist_plugin) 46 | 47 | 48 | def make_render_children(separator: str) -> Render: 49 | """Create a renderer that joins child nodes with a separator.""" 50 | 51 | def render_children( 52 | node: RenderTreeNode, 53 | context: RenderContext, 54 | ) -> str: 55 | return separator.join(child.render(context) for child in node.children) 56 | 57 | return render_children 58 | 59 | 60 | def render_material_definition_list( 61 | node: RenderTreeNode, 62 | context: RenderContext, 63 | ) -> str: 64 | """Render Material Definition List.""" 65 | return make_render_children("\n")(node, context) 66 | 67 | 68 | def render_material_definition_term( 69 | node: RenderTreeNode, 70 | context: RenderContext, 71 | ) -> str: 72 | """Render Material Definition Term.""" 73 | return make_render_children("\n")(node, context) 74 | 75 | 76 | def render_material_definition_body( 77 | node: RenderTreeNode, 78 | context: RenderContext, 79 | ) -> str: 80 | """Render the definition body.""" 81 | tight_list = all( 82 | child.type != "paragraph" or child.hidden for child in node.children 83 | ) 84 | marker = ": " # FYI: increased for material 85 | indent_width = len(marker) 86 | context.env["indent_width"] += indent_width 87 | try: 88 | text = make_render_children("\n\n")(node, context) 89 | lines = text.splitlines() 90 | if not lines: 91 | return ":" 92 | indented_lines = [f"{marker}{lines[0]}"] + [ 93 | f"{' ' * indent_width}{line}" if line else "" for line in lines[1:] 94 | ] 95 | joined_lines = ("" if tight_list else "\n") + "\n".join(indented_lines) 96 | next_sibling = node.next_sibling 97 | return joined_lines + ( 98 | "\n" if (next_sibling and next_sibling.type == "dt") else "" 99 | ) 100 | finally: 101 | context.env["indent_width"] -= indent_width 102 | 103 | 104 | def escape_deflist( 105 | text: str, 106 | node: RenderTreeNode, # noqa: ARG001 107 | context: RenderContext, # noqa: ARG001 108 | ) -> str: 109 | """Escape line starting ":" which would otherwise be parsed as a definition list.""" 110 | pattern = re.compile(r"^[:~] ") 111 | return "\n".join( 112 | "\\" + line if pattern.match(line) else line for line in text.split("\n") 113 | ) 114 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_mkdocstrings_autorefs.py: -------------------------------------------------------------------------------- 1 | """Match 'markdown anchors' from the `mkdocs-autorefs` plugin. 2 | 3 | Matches: 4 | 5 | ```md 6 | [](){#some-anchor-name} 7 | ``` 8 | 9 | Docs: https://mkdocstrings.github.io/autorefs 10 | 11 | """ 12 | 13 | from __future__ import annotations 14 | 15 | import re 16 | from re import Match 17 | from typing import TYPE_CHECKING 18 | 19 | from mdformat_mkdocs._synced.admon_factories import new_token 20 | 21 | if TYPE_CHECKING: 22 | from markdown_it import MarkdownIt 23 | from markdown_it.rules_block import StateBlock 24 | from markdown_it.rules_inline import StateInline 25 | 26 | _AUTOREFS_PATTERN = re.compile(r"\[\]\(<?>?\){#(?P<anchor>[^ }]+)}") 27 | _HEADING_PATTERN = re.compile(r"(?P<markdown>^#{1,6}) (?P<content>.+)") 28 | MKDOCSTRINGS_AUTOREFS_PREFIX = "mkdocstrings_autorefs" 29 | MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX = f"{MKDOCSTRINGS_AUTOREFS_PREFIX}_heading" 30 | 31 | 32 | def _mkdocstrings_autorefs_plugin(state: StateInline, silent: bool) -> bool: 33 | match = _AUTOREFS_PATTERN.match(state.src[state.pos : state.posMax]) 34 | if not match: 35 | return False 36 | 37 | if silent: 38 | return True 39 | 40 | anchor = match["anchor"] 41 | with new_token(state, MKDOCSTRINGS_AUTOREFS_PREFIX, "a") as token: 42 | token.attrs = {"id": anchor, "href": ""} 43 | token.meta = {"content": f"[](){{#{anchor}}}"} 44 | 45 | state.pos += match.end() 46 | 47 | return True 48 | 49 | 50 | def _mkdocstrings_heading_alias_plugin( 51 | state: StateBlock, 52 | start_line: int, 53 | end_line: int, 54 | silent: bool, 55 | ) -> bool: 56 | """Identify when an autoref anchor is directly before a heading. 57 | 58 | Simplified heading parsing adapted from: 59 | https://github.com/executablebooks/markdown-it-py/blob/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_block/heading.py 60 | 61 | """ 62 | if state.is_code_block(start_line): 63 | return False 64 | 65 | # Exit quickly if paragraph doesn't start with an autoref 66 | start = state.bMarks[start_line] + state.tShift[start_line] 67 | try: 68 | if state.src[start : start + 3] != "[](": 69 | return False 70 | except IndexError: 71 | return False 72 | 73 | matches: list[Match[str]] = [] 74 | heading: Match[str] | None = None 75 | next_line = start_line 76 | while next_line <= end_line: 77 | start = state.bMarks[next_line] + state.tShift[next_line] 78 | maximum = state.eMarks[next_line] 79 | line = state.src[start:maximum] 80 | # Catch as many sequential autorefs as possible before a single heading 81 | if match := _AUTOREFS_PATTERN.match(line): 82 | matches.append(match) 83 | else: 84 | heading = _HEADING_PATTERN.match(line) 85 | break 86 | next_line += 1 87 | if heading is None: # for pylint 88 | return False 89 | 90 | if silent: 91 | return True 92 | 93 | state.line = start_line + 1 94 | with new_token(state, MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX, "div"): 95 | for match in matches: 96 | anchor = match["anchor"] 97 | with new_token(state, MKDOCSTRINGS_AUTOREFS_PREFIX, "a") as a_token: 98 | a_token.attrs = {"id": anchor, "href": ""} 99 | a_token.meta = {"content": f"[](){{#{anchor}}}"} 100 | 101 | level = len(heading["markdown"]) 102 | with new_token(state, "heading", f"h{level}") as h_token: 103 | h_token.markup = heading["markdown"] 104 | h_token.map = [start_line, state.line] 105 | 106 | inline = state.push("inline", "", 0) 107 | inline.content = heading["content"] 108 | inline.map = [start_line, state.line] 109 | inline.children = [] 110 | 111 | state.line = next_line + 1 112 | 113 | return True 114 | 115 | 116 | def mkdocstrings_autorefs_plugin(md: MarkdownIt) -> None: 117 | md.inline.ruler.before( 118 | "link", 119 | MKDOCSTRINGS_AUTOREFS_PREFIX, 120 | _mkdocstrings_autorefs_plugin, 121 | {"alt": ["link"]}, 122 | ) 123 | md.block.ruler.before( 124 | "paragraph", 125 | MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX, 126 | _mkdocstrings_heading_alias_plugin, 127 | {"alt": ["paragraph"]}, 128 | ) 129 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_mkdocstrings_crossreference.py: -------------------------------------------------------------------------------- 1 | """Matches `mkdocstrings` cross-references. 2 | 3 | Matches: 4 | 5 | ```md 6 | [package.module.object][] 7 | [Object][package.module.object] 8 | ``` 9 | 10 | Docs: <https://mkdocstrings.github.io/usage/#cross-references> 11 | 12 | """ 13 | 14 | import re 15 | 16 | from markdown_it import MarkdownIt 17 | from markdown_it.rules_inline import StateInline 18 | 19 | from mdformat_mkdocs._synced.admon_factories import new_token 20 | 21 | _CROSSREFERENCE_PATTERN = re.compile(r"\[(?P<link>[^[|\]\n]+)\]\[(?P<href>[^\]\n]*)\]") 22 | MKDOCSTRINGS_CROSSREFERENCE_PREFIX = "mkdocstrings_crossreference" 23 | 24 | 25 | def _mkdocstrings_crossreference(state: StateInline, silent: bool) -> bool: 26 | match = _CROSSREFERENCE_PATTERN.match(state.src[state.pos : state.posMax]) 27 | if not match: 28 | return False 29 | 30 | if silent: 31 | return True 32 | 33 | original_pos = state.pos 34 | original_pos_max = state.posMax 35 | state.pos += 1 36 | state.posMax = state.pos + len(match["link"]) 37 | with new_token(state, MKDOCSTRINGS_CROSSREFERENCE_PREFIX, "a") as token: 38 | token.attrs = {"href": f"#{match['href'] or match['link']}"} 39 | token.meta = {"content": match.group()} 40 | 41 | state.linkLevel += 1 42 | state.md.inline.tokenize(state) 43 | state.linkLevel -= 1 44 | 45 | state.pos = original_pos 46 | state.posMax = original_pos_max 47 | state.pos += match.end() 48 | 49 | return True 50 | 51 | 52 | def mkdocstrings_crossreference_plugin(md: MarkdownIt) -> None: 53 | md.inline.ruler.push( 54 | MKDOCSTRINGS_CROSSREFERENCE_PREFIX, 55 | _mkdocstrings_crossreference, 56 | ) 57 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_pymd_abbreviations.py: -------------------------------------------------------------------------------- 1 | """Python-Markdown Abbreviations. 2 | 3 | Matches: 4 | 5 | ```md 6 | *[HTML]: Hyper Text Markup Language 7 | ``` 8 | 9 | Docs: 10 | https://github.com/Python-Markdown/markdown/blob/ec8c305fb14eb081bb874c917d8b91d3c5122334/docs/extensions/abbreviations.md 11 | 12 | 13 | """ 14 | 15 | from __future__ import annotations 16 | 17 | import re 18 | from typing import TYPE_CHECKING 19 | 20 | from mdit_py_plugins.utils import is_code_block 21 | 22 | from mdformat_mkdocs._synced.admon_factories import new_token 23 | 24 | if TYPE_CHECKING: 25 | from markdown_it import MarkdownIt 26 | from markdown_it.rules_block import StateBlock 27 | 28 | _ABBREVIATION_PATTERN = re.compile( 29 | r"\\?\*\\?\[(?P<label>[^\]\\]+)\\?\]: (?P<description>.+)", 30 | ) 31 | PYMD_ABBREVIATIONS_PREFIX = "mkdocs_abbreviation" 32 | 33 | 34 | def _new_match(state: StateBlock, start_line: int) -> re.Match[str] | None: 35 | """Determine match between start and end lines.""" 36 | start = state.bMarks[start_line] + state.tShift[start_line] 37 | maximum = state.eMarks[start_line] 38 | return _ABBREVIATION_PATTERN.match(state.src[start:maximum]) 39 | 40 | 41 | def _pymd_abbreviations( 42 | state: StateBlock, 43 | start_line: int, 44 | end_line: int, 45 | silent: bool, 46 | ) -> bool: 47 | """Identify syntax abbreviation syntax, but generates incorrect markup. 48 | 49 | To properly generate markup, the abbreviation descriptions would need to 50 | be stored in the state.env, but unlike markdown footnotes, the 51 | `mdkocs-abbreviations` aren't limited to the same file, so a full 52 | implementation in mdformat may not be possible, although someone more 53 | familiar with the library could probably find a way. 54 | 55 | If revisiting, the `mdformat-footnote` plugin is a great reference for how 56 | Material Abbreviations could be implemented in full: 57 | https://github.com/executablebooks/mdit-py-plugins/blob/d11bdaf0979e6fae01c35db5a4d1f6a4b4dd8843/mdit_py_plugins/footnote/index.py#L103-L198 58 | 59 | Additionally, reviewing the `python-markdown` implementation would likely 60 | be helpful: 61 | https://github.com/Python-Markdown/markdown/blob/ec8c305fb14eb081bb874c917d8b91d3c5122334/markdown/extensions/abbr.py 62 | 63 | """ 64 | if is_code_block(state, start_line): 65 | return False 66 | 67 | match = _new_match(state, start_line) 68 | if match is None: 69 | return False 70 | 71 | if silent: 72 | return True 73 | 74 | matches = [match] 75 | max_line = start_line 76 | while match is not None: 77 | if max_line == end_line: 78 | break 79 | if match := _new_match(state, max_line + 1): 80 | max_line += 1 81 | matches.append(match) 82 | 83 | with new_token(state, PYMD_ABBREVIATIONS_PREFIX, "p"): 84 | tkn_inline = state.push("inline", "", 0) 85 | tkn_inline.content = "\n".join( 86 | [f"*[{match['label']}]: {match['description']}" for match in matches], 87 | ) 88 | tkn_inline.map = [start_line, max_line] 89 | tkn_inline.children = [] 90 | 91 | state.line = max_line + 1 92 | 93 | return True 94 | 95 | 96 | def pymd_abbreviations_plugin(md: MarkdownIt) -> None: 97 | md.block.ruler.before( 98 | "reference", 99 | PYMD_ABBREVIATIONS_PREFIX, 100 | _pymd_abbreviations, 101 | {"alt": ["paragraph", "reference"]}, 102 | ) 103 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_pymd_admon.py: -------------------------------------------------------------------------------- 1 | """Python-Markdown Admonition Plugin. 2 | 3 | Copied from: https://github.com/KyleKing/mdformat-admon/blob/a5c965f867cda2256b3259f36ec36dda3b4bf831/mdformat_admon/mdit_plugins/_python_markdown_admon.py 4 | 5 | """ 6 | 7 | from markdown_it.rules_block import StateBlock 8 | 9 | from mdformat_mkdocs._synced.admon_factories import ( 10 | AdmonitionData, 11 | admon_plugin_factory, 12 | new_token, 13 | parse_possible_whitespace_admon_factory, 14 | parse_tag_and_title, 15 | ) 16 | 17 | PREFIX = "admonition" 18 | """Prefix used to differentiate the parsed output.""" 19 | 20 | 21 | def format_pymd_admon_markup( 22 | state: StateBlock, 23 | start_line: int, 24 | admonition: AdmonitionData, 25 | ) -> None: 26 | """Format markup.""" 27 | tags, title = parse_tag_and_title(admonition.meta_text) 28 | tag = tags[0] 29 | 30 | with new_token(state, PREFIX, "div") as token: 31 | token.markup = admonition.markup 32 | token.block = True 33 | token.attrs = {"class": " ".join(["admonition", *tags])} 34 | token.meta = {"tag": tag} 35 | token.info = admonition.meta_text 36 | token.map = [start_line, admonition.next_line] 37 | 38 | if title: 39 | title_markup = f"{admonition.markup} {tag}" 40 | with new_token(state, f"{PREFIX}_title", "p") as tkn_title: 41 | tkn_title.markup = title_markup 42 | tkn_title.attrs = {"class": "admonition-title"} 43 | tkn_title.map = [start_line, start_line + 1] 44 | 45 | tkn_inline = state.push("inline", "", 0) 46 | tkn_inline.content = title 47 | tkn_inline.map = [start_line, start_line + 1] 48 | tkn_inline.children = [] 49 | 50 | state.md.block.tokenize(state, start_line + 1, admonition.next_line) 51 | 52 | state.parentType = admonition.old_state.parentType 53 | state.lineMax = admonition.old_state.lineMax 54 | state.blkIndent = admonition.old_state.blkIndent 55 | state.line = admonition.next_line 56 | 57 | 58 | def admonition_logic( 59 | state: StateBlock, 60 | start_line: int, 61 | end_line: int, 62 | silent: bool, 63 | ) -> bool: 64 | """Parse Python Markdown-style Admonitions. 65 | 66 | `python-markdown style admonitions 67 | <https://python-markdown.github.io/extensions/admonition>`. 68 | 69 | .. code-block:: md 70 | 71 | !!! note 72 | *content* 73 | 74 | """ 75 | parse_possible_whitespace_admon = parse_possible_whitespace_admon_factory( 76 | markers={"!!!"}, 77 | ) 78 | result = parse_possible_whitespace_admon(state, start_line, end_line, silent) 79 | if isinstance(result, AdmonitionData): 80 | format_pymd_admon_markup(state, start_line, admonition=result) 81 | return True 82 | return result 83 | 84 | 85 | pymd_admon_plugin = admon_plugin_factory(PREFIX, admonition_logic) 86 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py: -------------------------------------------------------------------------------- 1 | r"""Python-Markdown Extensions: Arithmatex (Math Support). 2 | 3 | Uses existing mdit-py-plugins for LaTeX/MathJax mathematical expressions. 4 | 5 | Inline math delimiters: 6 | - $...$ (with smart_dollar rules: no whitespace adjacent to $) 7 | - \\(...\\) 8 | 9 | Block math delimiters: 10 | - $$...$$ 11 | - \\[...\\] 12 | - \\begin{env}...\\end{env} 13 | 14 | Docs: <https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex> 15 | 16 | """ 17 | 18 | from __future__ import annotations 19 | 20 | from typing import TYPE_CHECKING 21 | 22 | from mdit_py_plugins.amsmath import amsmath_plugin 23 | from mdit_py_plugins.dollarmath import dollarmath_plugin 24 | from mdit_py_plugins.texmath import texmath_plugin 25 | 26 | if TYPE_CHECKING: 27 | from markdown_it import MarkdownIt 28 | 29 | # Token types from the plugins 30 | # Note: dollarmath and texmath share the same token types for inline/block math: 31 | # - "math_inline" is used for both $...$ and \(...\) 32 | # - "math_block" is used for both $$...$$ and \[...\] 33 | DOLLARMATH_INLINE = "math_inline" 34 | DOLLARMATH_BLOCK = "math_block" 35 | DOLLARMATH_BLOCK_LABEL = "math_block_label" # For $$...$$ (label) syntax 36 | TEXMATH_BLOCK_EQNO = "math_block_eqno" # For \[...\] (label) syntax 37 | AMSMATH_BLOCK = "amsmath" 38 | 39 | 40 | def pymd_arithmatex_plugin(md: MarkdownIt) -> None: 41 | r"""Register Arithmatex support using existing mdit-py-plugins. 42 | 43 | This is a convenience wrapper that configures three existing plugins: 44 | - dollarmath_plugin: for $...$ and $$...$$ 45 | - texmath_plugin: for \\(...\\) and \\[...\\] 46 | - amsmath_plugin: for \\begin{env}...\\end{env} 47 | """ 48 | # Dollar syntax: $...$ and $$...$$ 49 | # Defaults provide smart dollar mode (no digits/space adjacent to $) 50 | md.use(dollarmath_plugin) 51 | 52 | # Bracket syntax: \(...\) and \[...\] 53 | md.use(texmath_plugin, delimiters="brackets") 54 | 55 | # LaTeX environments: \begin{env}...\end{env} 56 | md.use(amsmath_plugin) 57 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_pymd_captions.py: -------------------------------------------------------------------------------- 1 | """Python-Markdown Extensions Captions. 2 | 3 | Matches: 4 | 5 | ```md 6 | /// caption 7 | Default values for config variables. 8 | /// 9 | ``` 10 | 11 | Docs: 12 | https://github.com/facelessuser/pymdown-extensions/blob/main/pymdownx/blocks/caption.py 13 | 14 | 15 | """ 16 | 17 | from __future__ import annotations 18 | 19 | import re 20 | from typing import TYPE_CHECKING 21 | 22 | from mdit_py_plugins.utils import is_code_block 23 | 24 | from mdformat_mkdocs._synced.admon_factories._whitespace_admon_factories import ( 25 | new_token, 26 | ) 27 | 28 | if TYPE_CHECKING: 29 | from markdown_it import MarkdownIt 30 | from markdown_it.rules_block import StateBlock 31 | 32 | _CAPTION_START_PATTERN = re.compile( 33 | r"\s*///\s*(?P<type>figure-|table-|)caption\s*(\|\s*(?P<number>[\d\.]+))?", 34 | ) 35 | _CAPTION_END_PATTERN = re.compile(r"^\s*///\s*$") 36 | _CAPTION_ATTRS_PATTERN = re.compile(r"^s*(?P<attrs>attrs:\s*\{[^}]*\})\s*$") 37 | PYMD_CAPTIONS_PREFIX = "mkdocs_caption" 38 | 39 | 40 | def _src_in_line(state: StateBlock, line: int) -> tuple[str, int, int]: 41 | """Get the source in a given line number.""" 42 | start_pos = state.bMarks[line] + state.tShift[line] 43 | end_pos = state.eMarks[line] 44 | return state.src[start_pos:end_pos], start_pos, end_pos 45 | 46 | 47 | def _parse( 48 | state: StateBlock, 49 | first_line_max_pos: int, 50 | start_line: int, 51 | end_line: int, 52 | ) -> tuple[int, str, str | None]: 53 | """Parse a caption block: optionally read attrs and extract content.""" 54 | end_match = None 55 | max_line = start_line + 1 56 | end_pos = -1 57 | attrs_text, _, attrs_max_pos = _src_in_line(state, max_line) 58 | caption_attrs_match = _CAPTION_ATTRS_PATTERN.match(attrs_text) 59 | content_start_pos = ( 60 | first_line_max_pos + 1 if caption_attrs_match is None else attrs_max_pos + 1 61 | ) 62 | attrs = ( 63 | caption_attrs_match.group("attrs") if caption_attrs_match is not None else None 64 | ) 65 | if not isinstance(attrs, str): 66 | attrs = None 67 | 68 | while end_match is None and max_line <= end_line: 69 | line_text, end_pos, _ = _src_in_line(state, max_line) 70 | if _CAPTION_END_PATTERN.match(line_text) is None: 71 | max_line += 1 72 | else: 73 | end_match = max_line 74 | 75 | return max_line, state.src[content_start_pos:end_pos], attrs 76 | 77 | 78 | def _material_captions( 79 | state: StateBlock, 80 | start_line: int, 81 | end_line: int, 82 | silent: bool, 83 | ) -> bool: 84 | """Detect caption blocks and wrap them in a token.""" 85 | if is_code_block(state, start_line): 86 | return False 87 | 88 | first_line_text, _, first_line_max_pos = _src_in_line(state, start_line) 89 | start_match = _CAPTION_START_PATTERN.match(first_line_text) 90 | if start_match is None: 91 | return False 92 | 93 | if silent: 94 | return True 95 | 96 | max_line, content, attrs = _parse(state, first_line_max_pos, start_line, end_line) 97 | 98 | with ( 99 | new_token(state, PYMD_CAPTIONS_PREFIX, "figcaption") as token, 100 | new_token(state, "", "p"), 101 | ): 102 | token.info = start_match.group("type") + "caption" 103 | token.meta = {"number": start_match.group("number")} 104 | if attrs is not None: 105 | token.meta["attrs"] = attrs 106 | tkn_inline = state.push("inline", "", 0) 107 | tkn_inline.content = content.strip() 108 | tkn_inline.map = [start_line, max_line] 109 | tkn_inline.children = [] 110 | 111 | state.line = max_line + 1 112 | 113 | return True 114 | 115 | 116 | def pymd_captions_plugin(md: MarkdownIt) -> None: 117 | md.block.ruler.before( 118 | "fence", 119 | PYMD_CAPTIONS_PREFIX, 120 | _material_captions, 121 | {"alt": ["paragraph"]}, 122 | ) 123 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_pymd_snippet.py: -------------------------------------------------------------------------------- 1 | """Python-Markdown Extensions: Snippets. 2 | 3 | WARNING: matches only the "scissors" portion, leaving the rest unparsed 4 | 5 | ```md 6 | --8<-- ... 7 | ``` 8 | 9 | Docs: <https://facelessuser.github.io/pymdown-extensions/extensions/snippets> 10 | 11 | """ 12 | 13 | from __future__ import annotations 14 | 15 | import re 16 | from typing import TYPE_CHECKING 17 | 18 | from mdit_py_plugins.utils import is_code_block 19 | 20 | from mdformat_mkdocs._synced.admon_factories import new_token 21 | 22 | if TYPE_CHECKING: 23 | from markdown_it import MarkdownIt 24 | from markdown_it.rules_block import StateBlock 25 | 26 | _SNIPPET_MARKER = "--8<--" 27 | _ABBREVIATION_PATTERN = re.compile(rf"^{_SNIPPET_MARKER}(?P<label>.*)") 28 | 29 | PYMD_SNIPPET_PREFIX = "pymd_snippet" 30 | 31 | 32 | def _content(state: StateBlock, start_line: int) -> str: 33 | """Content.""" 34 | start = state.bMarks[start_line] + state.tShift[start_line] 35 | maximum = state.eMarks[start_line] 36 | return state.src[start:maximum] 37 | 38 | 39 | def _parse( 40 | state: StateBlock, 41 | match: re.Match[str], 42 | start_line: int, 43 | end_line: int, 44 | ) -> tuple[int, str]: 45 | """Return the max line and matched content.""" 46 | max_line = start_line + 1 47 | inline = f"{_SNIPPET_MARKER}{match['label']}" 48 | 49 | if not match["label"]: 50 | max_search = 20 # Upper limit of lines to search for multi-line snippet 51 | current_line = start_line + 1 52 | inner: list[str] = [] 53 | while (current_line - start_line) < max_search: 54 | if max_line == end_line: 55 | break # no 'label' 56 | line = _content(state, current_line) 57 | if _ABBREVIATION_PATTERN.match(line): 58 | max_line = current_line + 1 59 | inline = "\n".join([_SNIPPET_MARKER, *inner, _SNIPPET_MARKER]) 60 | break 61 | if line: 62 | inner.append(line) 63 | current_line += 1 64 | 65 | return max_line, inline 66 | 67 | 68 | def _pymd_snippet( 69 | state: StateBlock, 70 | start_line: int, 71 | end_line: int, 72 | silent: bool, 73 | ) -> bool: 74 | if is_code_block(state, start_line): 75 | return False 76 | 77 | match = _ABBREVIATION_PATTERN.match(_content(state, start_line)) 78 | if match is None: 79 | return False 80 | 81 | if silent: 82 | return True 83 | 84 | max_line, inline = _parse(state, match, start_line, end_line) 85 | 86 | with new_token(state, PYMD_SNIPPET_PREFIX, "p"): 87 | tkn_inline = state.push("inline", "", 0) 88 | tkn_inline.content = inline 89 | tkn_inline.map = [start_line, max_line] 90 | tkn_inline.children = [] 91 | 92 | state.line = max_line 93 | 94 | return True 95 | 96 | 97 | def pymd_snippet_plugin(md: MarkdownIt) -> None: 98 | md.block.ruler.before( 99 | "reference", 100 | PYMD_SNIPPET_PREFIX, 101 | _pymd_snippet, 102 | {"alt": ["paragraph"]}, 103 | ) 104 | -------------------------------------------------------------------------------- /mdformat_mkdocs/mdit_plugins/_python_markdown_attr_list.py: -------------------------------------------------------------------------------- 1 | r"""Python-Markdown: Attribute List. 2 | 3 | WARNING: does not properly render HTML with the attributes and does not 4 | respect escaping '\\{ ' 5 | 6 | Docs: <https://python-markdown.github.io/extensions/attr_list> 7 | 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import re 13 | from typing import TYPE_CHECKING 14 | 15 | from markdown_it import MarkdownIt 16 | 17 | from mdformat_mkdocs._synced.admon_factories import new_token 18 | 19 | if TYPE_CHECKING: 20 | from markdown_it import MarkdownIt 21 | from markdown_it.rules_inline import StateInline 22 | 23 | _ATTR_LIST_PATTERN = re.compile(r"{:? (?P<attrs>[^}]+) }") 24 | 25 | PYTHON_MARKDOWN_ATTR_LIST_PREFIX = "python_markdown_attr_list" 26 | 27 | 28 | def _python_markdown_attr_list(state: StateInline, silent: bool) -> bool: 29 | match = _ATTR_LIST_PATTERN.match(state.src[state.pos : state.posMax]) 30 | if not match: 31 | return False 32 | 33 | if state.pos > 0 and state.src[state.pos - 1] == "\\": 34 | return False 35 | 36 | if silent: 37 | return True 38 | 39 | original_pos = state.pos 40 | original_pos_max = state.posMax 41 | state.pos += 1 42 | state.posMax = state.pos + (match.end() - len(" }")) 43 | with new_token(state, PYTHON_MARKDOWN_ATTR_LIST_PREFIX, "span") as token: 44 | token.attrs = {"attributes": match["attrs"].split(" ")} # type: ignore[dict-item] 45 | token.meta = {"content": match.group()} 46 | 47 | state.md.inline.tokenize(state) 48 | 49 | state.pos = original_pos 50 | state.posMax = original_pos_max 51 | state.pos += match.end() 52 | 53 | return True 54 | 55 | 56 | def python_markdown_attr_list_plugin(md: MarkdownIt) -> None: 57 | md.inline.ruler.push( 58 | PYTHON_MARKDOWN_ATTR_LIST_PREFIX, 59 | _python_markdown_attr_list, 60 | ) 61 | -------------------------------------------------------------------------------- /mdformat_mkdocs/plugin.py: -------------------------------------------------------------------------------- 1 | """Public Extension.""" 2 | 3 | from __future__ import annotations 4 | 5 | import textwrap 6 | from functools import partial 7 | from typing import TYPE_CHECKING 8 | 9 | from mdformat.renderer import DEFAULT_RENDERERS, RenderContext, RenderTreeNode 10 | 11 | from ._helpers import ContextOptions, get_conf 12 | from ._normalize_list import normalize_list as unbounded_normalize_list 13 | from ._postprocess_inline import postprocess_list_wrap 14 | from .mdit_plugins import ( 15 | AMSMATH_BLOCK, 16 | DOLLARMATH_BLOCK, 17 | DOLLARMATH_BLOCK_LABEL, 18 | DOLLARMATH_INLINE, 19 | MKDOCSTRINGS_AUTOREFS_PREFIX, 20 | MKDOCSTRINGS_CROSSREFERENCE_PREFIX, 21 | MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX, 22 | PYMD_ABBREVIATIONS_PREFIX, 23 | PYMD_CAPTIONS_PREFIX, 24 | PYMD_SNIPPET_PREFIX, 25 | PYTHON_MARKDOWN_ATTR_LIST_PREFIX, 26 | TEXMATH_BLOCK_EQNO, 27 | escape_deflist, 28 | material_admon_plugin, 29 | material_content_tabs_plugin, 30 | material_deflist_plugin, 31 | mkdocstrings_autorefs_plugin, 32 | mkdocstrings_crossreference_plugin, 33 | pymd_abbreviations_plugin, 34 | pymd_admon_plugin, 35 | pymd_arithmatex_plugin, 36 | pymd_captions_plugin, 37 | pymd_snippet_plugin, 38 | python_markdown_attr_list_plugin, 39 | render_material_definition_body, 40 | render_material_definition_list, 41 | render_material_definition_term, 42 | ) 43 | 44 | if TYPE_CHECKING: 45 | import argparse 46 | from collections.abc import Mapping 47 | 48 | from markdown_it import MarkdownIt 49 | from mdformat.renderer.typing import Postprocess, Render 50 | 51 | 52 | def cli_is_ignore_missing_references(options: ContextOptions) -> bool: 53 | """user-specified flag to turn off bracket escaping when no link reference found. 54 | 55 | Addresses: https://github.com/KyleKing/mdformat-mkdocs/issues/19 56 | 57 | """ 58 | return bool(get_conf(options, "ignore_missing_references")) or False 59 | 60 | 61 | def cli_is_align_semantic_breaks_in_lists(options: ContextOptions) -> bool: 62 | """user-specified flag for toggling semantic breaks. 63 | 64 | - 3-spaces on subsequent lines in semantic numbered lists 65 | - and 2-spaces on subsequent bulleted items 66 | 67 | """ 68 | return bool(get_conf(options, "align_semantic_breaks_in_lists")) or False 69 | 70 | 71 | def cli_is_no_mkdocs_math(options: ContextOptions) -> bool: 72 | """user-specified flag to disable math/LaTeX rendering.""" 73 | return bool(get_conf(options, "no_mkdocs_math")) or False 74 | 75 | 76 | def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: 77 | """Add options to the mdformat CLI. 78 | 79 | Stored in `mdit.options["mdformat"]["plugin"]["mkdocs"]` 80 | 81 | """ 82 | group.add_argument( 83 | "--align-semantic-breaks-in-lists", 84 | action="store_const", 85 | const=True, 86 | help="If specified, align semantic indents in numbered and bulleted lists to the text", 87 | ) 88 | group.add_argument( 89 | "--ignore-missing-references", 90 | action="store_const", 91 | const=True, 92 | help="If set, do not escape link references when no definition is found. This is required when references are dynamic, such as with python mkdocstrings", 93 | ) 94 | group.add_argument( 95 | "--no-mkdocs-math", 96 | action="store_const", 97 | const=True, 98 | help="If set, disable math/LaTeX rendering (Arithmatex). By default, math is enabled.", 99 | ) 100 | 101 | 102 | def update_mdit(mdit: MarkdownIt) -> None: 103 | """Update the parser.""" 104 | mdit.use(material_admon_plugin) 105 | if not cli_is_no_mkdocs_math(mdit.options): 106 | mdit.use(pymd_arithmatex_plugin) 107 | mdit.use(pymd_captions_plugin) 108 | mdit.use(material_content_tabs_plugin) 109 | mdit.use(material_deflist_plugin) 110 | mdit.use(mkdocstrings_autorefs_plugin) 111 | mdit.use(pymd_abbreviations_plugin) 112 | mdit.use(pymd_admon_plugin) 113 | mdit.use(pymd_snippet_plugin) 114 | mdit.use(python_markdown_attr_list_plugin) 115 | 116 | if cli_is_ignore_missing_references(mdit.options): 117 | mdit.use(mkdocstrings_crossreference_plugin) 118 | 119 | 120 | def _render_node_content(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 121 | """Return node content without additional processing.""" 122 | return node.content 123 | 124 | 125 | def _render_math_inline(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 126 | """Render inline math with original delimiters.""" 127 | markup = node.markup 128 | content = node.content 129 | if markup == "$": 130 | return f"${content}$" 131 | if markup == "\\(": 132 | return f"\\({content}\\)" 133 | # Fallback 134 | return f"${content}$" 135 | 136 | 137 | def _render_math_block(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 138 | """Render block math with original delimiters.""" 139 | markup = node.markup 140 | content = node.content 141 | if markup == "$$": 142 | return f"$$\n{content.strip()}\n$$" 143 | if markup == "\\[": 144 | return f"\\[\n{content.strip()}\n\\]" 145 | # Fallback 146 | return f"$$\n{content.strip()}\n$$" 147 | 148 | 149 | def _render_math_block_eqno(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 150 | """Render block math with equation label.""" 151 | markup = node.markup 152 | content = node.content 153 | label = node.info # Label is stored in info field 154 | if markup == "$$": 155 | return f"$$\n{content.strip()}\n$$ ({label})" 156 | if markup == "\\[": 157 | return f"\\[\n{content.strip()}\n\\] ({label})" 158 | # Fallback 159 | return f"$$\n{content.strip()}\n$$ ({label})" 160 | 161 | 162 | def _render_amsmath(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 163 | """Render amsmath environment.""" 164 | # Content already includes \begin{} and \end{} 165 | return node.content 166 | 167 | 168 | def _render_meta_content(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 169 | """Return node content without additional processing.""" 170 | return node.meta.get("content", "") 171 | 172 | 173 | def _render_inline_content(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 174 | """Render the node's inline content.""" 175 | [inline] = node.children 176 | return inline.content 177 | 178 | 179 | def _render_code_inline(node: RenderTreeNode, context: RenderContext) -> str: 180 | r"""Render inline code, cleaning up whitespace from newline normalization. 181 | 182 | `markdown-it` normalizes newlines in inline code to spaces. This can result in 183 | unintended trailing spaces from original newlines before closing backticks. 184 | Per mdformat's own logic, trailing spaces are only intentional if there are 185 | also leading spaces. So we strip trailing spaces when there's no leading space. 186 | 187 | Example: `code\n` (newline) → `code ` (parsed) → `code` (rendered) 188 | 189 | This could break at any time, so this is a best effort to resolve issues like: 190 | https://github.com/KyleKing/mdformat-mkdocs/issues/34#issuecomment-3589835341 191 | 192 | """ 193 | default_renderer = DEFAULT_RENDERERS.get("code_inline") 194 | if default_renderer is None: 195 | return node.content 196 | 197 | result = default_renderer(node, context) 198 | 199 | # Only process single-backtick code (not double-backtick code with embedded backticks) 200 | if not (result.startswith("`") and result.endswith("`") and "``" not in result): 201 | return result 202 | 203 | content = result[1:-1] # Strip opening and closing backticks 204 | has_leading_space = content.startswith(" ") 205 | has_trailing_space = content.endswith(" ") 206 | 207 | # Strip trailing space only if there's no leading space and content is not all whitespace 208 | # This preserves the mdformat rule: spaces are only intentional when both are present 209 | if has_trailing_space and not has_leading_space and content.strip(): 210 | return f"`{content.rstrip(' ')}`" 211 | 212 | return result 213 | 214 | 215 | def _render_heading_autoref(node: RenderTreeNode, context: RenderContext) -> str: 216 | """Render autorefs directly above a heading.""" 217 | [*autorefs, heading] = node.children 218 | lines = [_render_meta_content(_n, context) for _n in autorefs] 219 | lines.append(f"{heading.markup} {_render_inline_content(heading, context)}") 220 | return "\n".join(lines) 221 | 222 | 223 | def _render_with_default_renderer( 224 | node: RenderTreeNode, 225 | context: RenderContext, 226 | syntax_type: str, 227 | ) -> str: 228 | """Attempt to render using the mdformat DEFAULT. 229 | 230 | Adapted from: 231 | https://github.com/hukkin/mdformat-gfm/blob/bd3c3392830fc4805d51582adcd1ae0d0630aed4/src/mdformat_gfm/plugin.py#L35-L46 232 | 233 | """ 234 | text = DEFAULT_RENDERERS.get(syntax_type, _render_node_content)(node, context) 235 | for postprocessor in context.postprocessors.get(syntax_type, ()): 236 | text = postprocessor(text, node, context) 237 | return text 238 | 239 | 240 | def _render_cross_reference(node: RenderTreeNode, context: RenderContext) -> str: 241 | """Render a MkDocs crossreference link.""" 242 | if cli_is_ignore_missing_references(context.options): 243 | return _render_meta_content(node, context) 244 | # Default to treating the matched content as a link 245 | return _render_with_default_renderer(node, context, "link") 246 | 247 | 248 | # Start: copied from mdformat-admon 249 | 250 | 251 | def render_admon(node: RenderTreeNode, context: RenderContext) -> str: 252 | """Render a `RenderTreeNode` of type `admonition`.""" 253 | prefix = node.markup.split(" ")[0] 254 | title = node.info.strip() 255 | title_line = f"{prefix} {title}" 256 | 257 | elements = [render for child in node.children if (render := child.render(context))] 258 | separator = "\n\n" 259 | 260 | # Then indent to either 3 or 4 based on the length of the prefix 261 | # For reStructuredText, '..' should be indented 3-spaces 262 | # While '!!!', , '...', '???', '???+', etc. are indented 4-spaces 263 | indent = " " * (min(len(prefix), 3) + 1) 264 | content = textwrap.indent(separator.join(elements), indent) 265 | 266 | return title_line + "\n" + content if content else title_line 267 | 268 | 269 | def render_admon_title( 270 | node: RenderTreeNode, # noqa: ARG001 271 | context: RenderContext, # noqa: ARG001 272 | ) -> str: 273 | """Skip rendering the title when called from the `node.children`.""" 274 | return "" 275 | 276 | 277 | # End: copied from mdformat-admon 278 | 279 | 280 | def add_extra_admon_newline(node: RenderTreeNode, context: RenderContext) -> str: 281 | """Return admonition with additional newline after the title for mkdocs.""" 282 | result = render_admon(node, context) 283 | if "\n" not in result: 284 | return result 285 | title, *content = result.split("\n", maxsplit=1) 286 | return f"{title}\n\n{''.join(content)}" 287 | 288 | 289 | def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str: 290 | """Render caption with normalized format.""" 291 | caption_type = node.info or "caption" 292 | attrs = node.meta.get("attrs") 293 | number = node.meta.get("number") 294 | rendered_content = "".join( 295 | child.render(context) for child in node.children[0].children 296 | ) 297 | caption_number = f" | {number}" if number else "" 298 | caption_attrs = f"\n {attrs}" if attrs else "" 299 | return f"/// {caption_type}{caption_number}{caption_attrs}\n{rendered_content}\n///" 300 | 301 | 302 | # A mapping from syntax tree node type to a function that renders it. 303 | # This can be used to overwrite renderer functions of existing syntax 304 | # or add support for new syntax. 305 | RENDERERS: Mapping[str, Render] = { 306 | "admonition": add_extra_admon_newline, 307 | "admonition_title": render_admon_title, 308 | "admonition_mkdocs": add_extra_admon_newline, 309 | "admonition_mkdocs_title": render_admon_title, 310 | "code_inline": _render_code_inline, 311 | "content_tab_mkdocs": add_extra_admon_newline, 312 | "content_tab_mkdocs_title": render_admon_title, 313 | "dl": render_material_definition_list, 314 | "dt": render_material_definition_term, 315 | "dd": render_material_definition_body, 316 | # Math support (from mdit-py-plugins) 317 | DOLLARMATH_INLINE: _render_math_inline, 318 | DOLLARMATH_BLOCK: _render_math_block, 319 | DOLLARMATH_BLOCK_LABEL: _render_math_block_eqno, 320 | TEXMATH_BLOCK_EQNO: _render_math_block_eqno, 321 | AMSMATH_BLOCK: _render_amsmath, 322 | # Other plugins 323 | PYMD_CAPTIONS_PREFIX: render_pymd_caption, 324 | MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content, 325 | MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference, 326 | MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX: _render_heading_autoref, 327 | PYMD_ABBREVIATIONS_PREFIX: _render_inline_content, 328 | PYMD_SNIPPET_PREFIX: _render_inline_content, 329 | PYTHON_MARKDOWN_ATTR_LIST_PREFIX: _render_meta_content, 330 | } 331 | 332 | 333 | normalize_list = partial( 334 | unbounded_normalize_list, # type: ignore[has-type] 335 | check_if_align_semantic_breaks_in_lists=cli_is_align_semantic_breaks_in_lists, 336 | ) 337 | 338 | # A mapping from `RenderTreeNode.type` to a `Postprocess` that does 339 | # postprocessing for the output of the `Render` function. Unlike 340 | # `Render` funcs, `Postprocess` funcs are collaborative: any number of 341 | # plugins can define a postprocessor for a syntax type and all of them 342 | # will run in series. 343 | POSTPROCESSORS: Mapping[str, Postprocess] = { 344 | "bullet_list": normalize_list, 345 | "inline": postprocess_list_wrap, # type: ignore[has-type] 346 | "ordered_list": normalize_list, 347 | "paragraph": escape_deflist, 348 | } 349 | -------------------------------------------------------------------------------- /mdformat_mkdocs/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdsf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/hougesen/mdsf/main/schemas/v0.11.0/mdsf.schema.json", 3 | "languages": { 4 | "go": "gofmt", 5 | "javascript": [ 6 | [ 7 | "deno:fmt", 8 | "prettier" 9 | ] 10 | ], 11 | "json": [ 12 | [ 13 | "deno:fmt", 14 | "prettier" 15 | ] 16 | ], 17 | "python": "ruff:format", 18 | "shell": [ 19 | [ 20 | "shfmt", 21 | "beautysh" 22 | ] 23 | ], 24 | "sql": "sqlfluff:format", 25 | "toml": "toml-sort", 26 | "typescript": [ 27 | [ 28 | "deno:fmt", 29 | "prettier" 30 | ] 31 | ], 32 | "yaml": "prettier" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mise.lock: -------------------------------------------------------------------------------- 1 | [[tools."cargo:mdsf"]] 2 | version = "0.11.0" 3 | backend = "cargo:mdsf" 4 | 5 | [[tools."pipx:prek"]] 6 | version = "0.2.18" 7 | backend = "pipx:prek" 8 | 9 | [[tools."pipx:pytest-watcher"]] 10 | version = "0.4.3" 11 | backend = "pipx:pytest-watcher" 12 | 13 | [[tools."pipx:tox"]] 14 | version = "4.32.0" 15 | backend = "pipx:tox" 16 | 17 | [[tools.python]] 18 | version = "3.10.15" 19 | backend = "core:python" 20 | 21 | [[tools.python]] 22 | version = "3.14.0" 23 | backend = "core:python" 24 | 25 | [[tools.uv]] 26 | version = "0.9.11" 27 | backend = "aqua:astral-sh/uv" 28 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | "cargo:mdsf" = "latest" 3 | "pipx:prek" = "latest" 4 | "pipx:pytest-watcher" = "latest" 5 | "pipx:tox" = {uvx_args = "--with tox-uv==1.29.0", version = "4.32.0"} 6 | python = ["3.10.15", "3.14.0"] 7 | uv = "latest" 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "uv_build" 3 | requires = ["uv_build>=0.9.10"] 4 | 5 | [project] 6 | authors = [ 7 | {email = "dev.act.kyle@gmail.com", name = "kyleking"}, 8 | ] 9 | classifiers = [ 10 | "Intended Audience :: Developers", 11 | "License :: OSI Approved :: MIT License", 12 | "Programming Language :: Python :: 3", 13 | "Topic :: Software Development :: Libraries :: Python Modules", 14 | ] 15 | dependencies = [ 16 | "mdformat >= 0.7.19", 17 | "mdformat-gfm >= 0.3.6", 18 | "mdit-py-plugins >= 0.4.1", 19 | "more-itertools >= 10.5.0", 20 | ] 21 | description = "An mdformat plugin for mkdocs and Material for MkDocs" 22 | keywords = ["markdown", "markdown-it", "mdformat", "mdformat_plugin_template"] 23 | license = "MIT" 24 | license-files = ["LICENSE"] 25 | name = "mdformat_mkdocs" 26 | readme = "README.md" 27 | requires-python = ">=3.10.0" 28 | version = "5.1.1" 29 | 30 | [project.entry-points."mdformat.parser_extension"] 31 | mkdocs = "mdformat_mkdocs" 32 | 33 | [project.optional-dependencies] 34 | recommended = [ 35 | # Keep in-sync with README 36 | "mdformat-beautysh >= 0.1.1", 37 | "mdformat-config >= 0.2.1", 38 | "mdformat-footnote >= 0.1.1", 39 | "mdformat-front-matters >= 0.1.0", 40 | "mdformat-gfm >=1.0.0", 41 | "mdformat-ruff >= 0.1.3", 42 | "mdformat-simple-breaks >= 0.0.1", 43 | "mdformat-web >= 0.2.0", 44 | "mdformat-wikilink >= 0.2.0", 45 | # Patches https://github.com/lovesegfault/beautysh/issues/248 for Python 3.12 46 | "setuptools", 47 | ] 48 | recommended-mdsf = [ 49 | # Keep in-sync with README 50 | "mdformat-footnote >= 0.1.0", 51 | "mdformat-front-matters >= 1.0.1", 52 | "mdformat-gfm >=1.0.0", 53 | "mdformat-hooks >= 0.1.0", 54 | "mdformat-simple-breaks >= 0.0.1", 55 | "mdformat-wikilink >= 0.2.0", 56 | ] 57 | test = [ 58 | "beartype >= 0.21.0", 59 | "pytest >= 9.0.1", 60 | "pytest-beartype >= 0.2.0", 61 | "pytest-cov >= 7.0.0", 62 | "syrupy >= 4.9.1", 63 | ] 64 | 65 | [project.urls] 66 | "Bug Tracker" = "https://github.com/kyleking/mdformat-mkdocs/issues" 67 | "Changelog" = "https://github.com/kyleking/mdformat-mkdocs/releases" 68 | homepage = "https://github.com/kyleking/mdformat-mkdocs" 69 | 70 | [tool.commitizen] 71 | tag_format = "v${version}" 72 | version = "5.1.1" 73 | version_files = ["mdformat_mkdocs/__init__.py", "pyproject.toml:^version"] 74 | 75 | [tool.mypy] 76 | check_untyped_defs = true 77 | disallow_any_generics = true 78 | enable_error_code = ["ignore-without-code", "possibly-undefined", "redundant-expr", "truthy-bool"] 79 | extra_checks = true 80 | files = ["mdformat_mkdocs", "tests"] 81 | no_implicit_reexport = true 82 | python_version = "3.10" 83 | show_column_numbers = true 84 | show_error_codes = true 85 | strict_equality = true 86 | warn_no_return = true 87 | warn_redundant_casts = true 88 | warn_unreachable = true 89 | warn_unused_configs = true 90 | warn_unused_ignores = true 91 | 92 | [tool.pyright] 93 | include = ["mdformat_mkdocs", "tests"] 94 | pythonVersion = "3.10" 95 | 96 | [tool.pytest-watcher] 97 | ignore_patterns = [] 98 | now = true 99 | patterns = ["*.ambr", "*.md", "*.py"] 100 | runner = "tox" 101 | # PLANNED: requires support for TYPE_CHECKING https://github.com/beartype/beartype/issues/477 102 | # runner_args = ["-e", "test", "--", "--exitfirst", "--failed-first", "--new-first", "-vv", "--beartype-packages=mdformat_mkdocs"] 103 | runner_args = ["-e", "test", "--", "--exitfirst", "--failed-first", "--new-first", "-vv"] 104 | 105 | [tool.ruff] 106 | # Docs: https://github.com/charliermarsh/ruff 107 | # Tip: poetry run python -m ruff --explain RUF100 108 | line-length = 88 109 | target-version = 'py310' 110 | 111 | [tool.ruff.lint] 112 | ignore = [ 113 | 'ANN002', # Missing type annotation for `*args` 114 | 'ANN003', # Missing type annotation for `**kwargs` 115 | 'BLE001', # Do not catch blind exception: `Exception` 116 | 'COM812', # missing-trailing-comma 117 | 'CPY001', # Missing copyright notice at top of file 118 | 'D203', # "1 blank line required before class docstring" (Conflicts with D211) 119 | 'D213', # "Multi-line docstring summary should start at the second line" (Conflicts with D212) 120 | 'DOC201', # `return` is not documented in docstring 121 | 'DOC402', # `yield` is not documented in docstring 122 | 'E501', # Line too long (89 > 88) 123 | 'EM101', # Exception must not use a string literal, assign to variable first 124 | 'FBT001', # Boolean-typed positional argument in function definition 125 | 'FIX001', # Line contains FIXME 126 | 'FIX002', # Line contains TODO 127 | 'FIX004', # Line contains HACK 128 | 'N803', # Argument name `startLine` should be lowercase 129 | 'N815', # Variable `lineMax` in class scope should not be mixedCase 130 | 'PLR0913', # Too many arguments in function definition (6 > 5) 131 | 'S101', # Use of `assert` detected 132 | 'TC002', # Move third-party import `mdformat.renderer.typing.Postprocess` into a type-checking block (for beartype) 133 | 'TC003', # Move standard library import `argparse` into a type-checking block (for beartype) 134 | 'TD001', # Invalid TODO tag: `FIXME` 135 | 'TD002', # Missing author in TODO; try: `# TODO(<author_name>): ...` 136 | 'TD003', # Missing issue link on the line following this TODO 137 | 'TRY003', # Avoid specifying long messages outside the exception class 138 | ] 139 | preview = true 140 | select = ['ALL'] 141 | unfixable = [ 142 | 'ERA001', # Commented out code 143 | ] 144 | 145 | [tool.ruff.lint.isort] 146 | known-first-party = ['mdformat_mkdocs', 'tests'] 147 | 148 | [tool.ruff.lint.per-file-ignores] 149 | '__init__.py' = [ 150 | 'D104', # Missing docstring in public package 151 | ] 152 | 'tests/*.py' = [ 153 | 'ANN001', # Missing type annotation for function argument 154 | 'ANN201', # Missing return type annotation for public function 155 | 'ANN202', # Missing return type annotation for private function `test_make_diffable` 156 | 'ARG001', # Unused function argument: `line` 157 | 'D100', # Missing docstring in public module 158 | 'D103', # Missing docstring in public function 159 | 'PLC2701', # Private name import `_<>` from external module 160 | 'PT004', # flake8-pytest-style: fixture does not return 161 | 'S101', # Use of `assert` detected 162 | ] 163 | 164 | [tool.ruff.lint.pydocstyle] 165 | convention = "google" 166 | 167 | [tool.tomlsort] 168 | all = true 169 | in_place = true 170 | trailing_comma_inline_array = true 171 | 172 | [tool.tomlsort.overrides."tool.pytest-watcher.*"] 173 | inline_arrays = false 174 | 175 | [tool.tomlsort.overrides."tool.tox.env.*"] 176 | inline_arrays = false 177 | 178 | [tool.tox] 179 | # Docs: https://tox.wiki/en/4.23.2/config.html#core 180 | basepython = ["py310", "py314"] 181 | env_list = ["hook-min", "prek", "ruff", "test-min", "type"] 182 | isolated_build = true 183 | requires = ["tox>=4.32.0"] 184 | skip_missing_interpreters = false 185 | 186 | [tool.tox.env.cz] 187 | basepython = ["py314"] 188 | commands = [["cz", "bump", {default = [], extend = true, replace = "posargs"}]] 189 | deps = "commitizen>=4.10.0" 190 | description = "Bump version using commitizen. Optionally specify: '-- --increment MAJOR|MINOR|PATCH' or '-- --dry-run'" 191 | skip_install = true 192 | 193 | [tool.tox.env."hook-min"] 194 | basepython = ["py310"] 195 | commands = [["prek", "run", "--config=.pre-commit-test.yaml", "--all-files", {default = ["--show-diff-on-failure", "--verbose"], extend = true, replace = "posargs"}]] 196 | deps = "prek>=0.2.17" 197 | 198 | [tool.tox.env.prek] 199 | basepython = ["py314"] 200 | commands = [["prek", "run", "--all-files", {default = [], extend = true, replace = "posargs"}]] 201 | deps = "prek>=0.2.17" 202 | skip_install = true 203 | 204 | [tool.tox.env.ruff] 205 | basepython = ["py314"] 206 | commands = [ 207 | ["ruff", "check", ".", "--fix", {default = [], extend = true, replace = "posargs"}], 208 | ["ruff", "format", "."], 209 | ] 210 | deps = "ruff>=0.14.5" 211 | description = "Optionally, specify: '-- --unsafe-fixes'" 212 | skip_install = true 213 | 214 | [tool.tox.env.test] 215 | basepython = ["py314"] 216 | commands = [["pytest", "--cov=mdformat_mkdocs", {default = [], extend = true, replace = "posargs"}]] 217 | # PLANNED: requires support for TYPE_CHECKING https://github.com/beartype/beartype/issues/477 218 | # description = "Optionally, specify: '-- --exitfirst --failed-first --new-first -vv --beartype-packages=mdformat_mkdocs" 219 | description = "Optionally, specify: '-- --exitfirst --failed-first --new-first -vv" 220 | extras = ["test"] 221 | 222 | [tool.tox.env."test-min"] 223 | basepython = ["py310"] 224 | commands = [["pytest", "--cov=mdformat_mkdocs"]] 225 | extras = ["test"] 226 | 227 | [tool.tox.env.type] 228 | basepython = ["py314"] 229 | commands = [["mypy", "./mdformat_mkdocs", {default = [], extend = true, replace = "posargs"}]] 230 | deps = ["mypy>=1.18.2"] 231 | 232 | [tool.tox.env_run_base] 233 | # Validates that commands are set 234 | commands = [["error-commands-are-not-set"]] 235 | 236 | [tool.uv.build-backend] 237 | module-root = "" 238 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Ignore beartype warnings from tests.""" 2 | 3 | from contextlib import suppress 4 | 5 | with suppress(ImportError): # Only suppress beartype warnings when installed 6 | from warnings import filterwarnings 7 | 8 | from beartype.roar import ( 9 | BeartypeClawDecorWarning, # Too many False Positives using NamedTuples 10 | BeartypeDecorHintPep585DeprecationWarning, 11 | ) 12 | 13 | filterwarnings("ignore", category=BeartypeClawDecorWarning) 14 | filterwarnings("ignore", category=BeartypeDecorHintPep585DeprecationWarning) 15 | -------------------------------------------------------------------------------- /tests/format/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/format/fixtures/material_content_tabs.md: -------------------------------------------------------------------------------- 1 | Do not modify multi-line code from: https://github.com/KyleKing/mdformat-mkdocs/issues/23 2 | . 3 | === "duty" 4 | ```python title="duties.py" 5 | @duty(silent=True) 6 | def coverage(ctx): 7 | ctx.run("coverage combine", nofail=True) 8 | ctx.run("coverage report --rcfile=config/coverage.ini", capture=False) 9 | ctx.run("coverage html --rcfile=config/coverage.ini") 10 | 11 | 12 | @duty 13 | def test(ctx, match: str = ""): 14 | py_version = f"{sys.version_info.major}{sys.version_info.minor}" 15 | os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" 16 | ctx.run( 17 | ["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, "tests"], 18 | title="Running tests", 19 | ) 20 | ``` 21 | . 22 | === "duty" 23 | 24 | ```python title="duties.py" 25 | @duty(silent=True) 26 | def coverage(ctx): 27 | ctx.run("coverage combine", nofail=True) 28 | ctx.run("coverage report --rcfile=config/coverage.ini", capture=False) 29 | ctx.run("coverage html --rcfile=config/coverage.ini") 30 | 31 | 32 | @duty 33 | def test(ctx, match: str = ""): 34 | py_version = f"{sys.version_info.major}{sys.version_info.minor}" 35 | os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" 36 | ctx.run( 37 | ["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, "tests"], 38 | title="Running tests", 39 | ) 40 | ``` 41 | . 42 | -------------------------------------------------------------------------------- /tests/format/fixtures/material_deflist.md: -------------------------------------------------------------------------------- 1 | Pandoc (with slightly changed indents): 2 | . 3 | paragraph 4 | 5 | Term 1 6 | 7 | : Definition 1 8 | 9 | Term 2 with *inline markup* 10 | 11 | : Definition 2 12 | 13 | { some code, part of Definition 2 } 14 | 15 | Third paragraph of definition 2. 16 | 17 | paragraph 18 | . 19 | paragraph 20 | 21 | Term 1 22 | 23 | : Definition 1 24 | 25 | Term 2 with *inline markup* 26 | 27 | : Definition 2 28 | 29 | ``` 30 | { some code, part of Definition 2 } 31 | ``` 32 | 33 | Third paragraph of definition 2. 34 | 35 | paragraph 36 | . 37 | 38 | Pandoc 2: 39 | . 40 | Term 1 41 | 42 | : Definition 43 | with lazy continuation. 44 | 45 | Second paragraph of the definition. 46 | . 47 | Term 1 48 | 49 | : Definition 50 | with lazy continuation. 51 | 52 | Second paragraph of the definition. 53 | . 54 | 55 | Pandoc 3 56 | . 57 | paragraph 58 | 59 | Term 1 60 | ~ Definition 1 61 | 62 | Term 2 63 | ~ Definition 2a 64 | ~ Definition 2b 65 | 66 | paragraph 67 | . 68 | paragraph 69 | 70 | Term 1 71 | : Definition 1 72 | 73 | Term 2 74 | : Definition 2a 75 | : Definition 2b 76 | 77 | paragraph 78 | . 79 | 80 | Spaces after a colon: 81 | . 82 | Term 1 83 | : paragraph 84 | 85 | Term 2 86 | : code block 87 | . 88 | Term 1 89 | : paragraph 90 | 91 | Term 2 92 | : ``` 93 | code block 94 | ``` 95 | . 96 | 97 | List is tight, only if all dts are tight: 98 | . 99 | Term 1 100 | : foo 101 | : bar 102 | 103 | Term 2 104 | : foo 105 | 106 | : bar 107 | . 108 | Term 1 109 | 110 | : foo 111 | 112 | : bar 113 | 114 | Term 2 115 | 116 | : foo 117 | 118 | : bar 119 | . 120 | 121 | 122 | Regression test (first paragraphs shouldn't be tight): 123 | . 124 | Term 1 125 | : foo 126 | 127 | bar 128 | 129 | Term 2 130 | : foo 131 | . 132 | Term 1 133 | 134 | : foo 135 | 136 | bar 137 | 138 | Term 2 139 | 140 | : foo 141 | . 142 | 143 | Nested definition lists: 144 | 145 | . 146 | test 147 | : foo 148 | : bar 149 | : baz 150 | : bar 151 | : foo 152 | . 153 | test 154 | : foo 155 | : bar 156 | : baz 157 | : bar 158 | : foo 159 | . 160 | 161 | Regression test (blockquote inside deflist) 162 | . 163 | foo 164 | : > bar 165 | : baz 166 | . 167 | foo 168 | : > bar 169 | : baz 170 | . 171 | 172 | Escaped deflist 173 | . 174 | Term 1 175 | \: Definition 176 | . 177 | Term 1 178 | \: Definition 179 | . 180 | -------------------------------------------------------------------------------- /tests/format/fixtures/material_math.md: -------------------------------------------------------------------------------- 1 | Block Math with Double Dollar 2 | . 3 | The cosine series expansion: 4 | 5 | $$ 6 | \cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} 7 | $$ 8 | 9 | This is a fundamental trigonometric identity. 10 | . 11 | The cosine series expansion: 12 | 13 | $$ 14 | \cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} 15 | $$ 16 | 17 | This is a fundamental trigonometric identity. 18 | . 19 | 20 | Block Math with Square Brackets 21 | . 22 | The same formula using LaTeX bracket notation: 23 | 24 | \[ 25 | \cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} 26 | \] 27 | 28 | This demonstrates equivalent notation. 29 | . 30 | The same formula using LaTeX bracket notation: 31 | 32 | \[ 33 | \cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} 34 | \] 35 | 36 | This demonstrates equivalent notation. 37 | . 38 | 39 | Inline Math with Dollar Signs 40 | . 41 | The homomorphism $f$ is injective if and only if its kernel is only the singleton set $e_G$, because otherwise $\exists a,b\in G$ with $a\neq b$ such that $f(a)=f(b)$. 42 | 43 | This shows how inline math integrates with text. 44 | . 45 | The homomorphism $f$ is injective if and only if its kernel is only the singleton set $e_G$, because otherwise $\exists a,b\in G$ with $a\neq b$ such that $f(a)=f(b)$. 46 | 47 | This shows how inline math integrates with text. 48 | . 49 | 50 | Inline Math with Parentheses 51 | . 52 | Using LaTeX parenthesis notation: \(f\) is injective if and only if its kernel is only the singleton set \(e_G\), because otherwise \(\exists a,b\in G\) with \(a\neq b\) such that \(f(a)=f(b)\). 53 | 54 | This demonstrates alternative inline notation. 55 | . 56 | Using LaTeX parenthesis notation: \(f\) is injective if and only if its kernel is only the singleton set \(e_G\), because otherwise \(\exists a,b\in G\) with \(a\neq b\) such that \(f(a)=f(b)\). 57 | 58 | This demonstrates alternative inline notation. 59 | . 60 | 61 | Mixed Math Syntax 62 | . 63 | Combining different notations: the function $f: G \to H$ satisfies \(f(a \cdot b) = f(a) \cdot f(b)\) for all $a,b \in G$. 64 | 65 | $$ 66 | f(a \cdot b) = f(a) \cdot f(b) 67 | $$ 68 | 69 | This shows how different delimiters can be mixed. 70 | . 71 | Combining different notations: the function $f: G \to H$ satisfies \(f(a \cdot b) = f(a) \cdot f(b)\) for all $a,b \in G$. 72 | 73 | $$ 74 | f(a \cdot b) = f(a) \cdot f(b) 75 | $$ 76 | 77 | This shows how different delimiters can be mixed. 78 | . 79 | -------------------------------------------------------------------------------- /tests/format/fixtures/math_with_mkdocs_features.md: -------------------------------------------------------------------------------- 1 | Math with Admonitions 2 | . 3 | !!! note 4 | The equation $E = mc^2$ is famous. 5 | 6 | $$ 7 | E = mc^2 8 | $$ 9 | 10 | !!! tip "Energy Formula" 11 | Einstein's equation \(E = mc^2\) relates energy and mass. 12 | 13 | \[ 14 | E = mc^2 15 | \] 16 | . 17 | !!! note 18 | 19 | The equation $E = mc^2$ is famous. 20 | 21 | $$ 22 | E = mc^2 23 | $$ 24 | 25 | !!! tip "Energy Formula" 26 | 27 | Einstein's equation \(E = mc^2\) relates energy and mass. 28 | 29 | \[ 30 | E = mc^2 31 | \] 32 | . 33 | 34 | Math with Content Tabs 35 | . 36 | === "Tab 1" 37 | Formula: $x = y$ 38 | 39 | Block equation: 40 | 41 | $$ 42 | a^2 + b^2 = c^2 43 | $$ 44 | 45 | === "Tab 2" 46 | Using parenthesis notation: \(F = ma\) 47 | 48 | \[ 49 | F = ma 50 | \] 51 | . 52 | === "Tab 1" 53 | 54 | Formula: $x = y$ 55 | 56 | Block equation: 57 | 58 | $$ 59 | a^2 + b^2 = c^2 60 | $$ 61 | 62 | === "Tab 2" 63 | 64 | Using parenthesis notation: \(F = ma\) 65 | 66 | \[ 67 | F = ma 68 | \] 69 | . 70 | 71 | Math with Definition Lists 72 | . 73 | Einstein's Equation 74 | : $E = mc^2$ represents energy-mass equivalence. 75 | 76 | $$ 77 | E = mc^2 78 | $$ 79 | 80 | Newton's Second Law 81 | : The force \(F\) is defined as: 82 | 83 | \[ 84 | F = ma 85 | \] 86 | . 87 | Einstein's Equation 88 | 89 | : $E = mc^2$ represents energy-mass equivalence. 90 | 91 | $$ 92 | E = mc^2 93 | $$ 94 | 95 | Newton's Second Law 96 | 97 | : The force \(F\) is defined as: 98 | 99 | \[ 100 | F = ma 101 | \] 102 | . 103 | 104 | Math in Nested Admonitions 105 | . 106 | !!! note "Outer Note" 107 | This is the outer admonition with $x = y$. 108 | 109 | !!! warning "Inner Warning" 110 | Nested admonition with \(a = b\). 111 | 112 | $$ 113 | c = d 114 | $$ 115 | 116 | Back to outer with: 117 | 118 | \[ 119 | e = f 120 | \] 121 | . 122 | !!! note "Outer Note" 123 | 124 | This is the outer admonition with $x = y$. 125 | 126 | !!! warning "Inner Warning" 127 | 128 | Nested admonition with \(a = b\). 129 | 130 | $$ 131 | c = d 132 | $$ 133 | 134 | Back to outer with: 135 | 136 | \[ 137 | e = f 138 | \] 139 | . 140 | 141 | Math in Admonitions with Lists 142 | . 143 | !!! info 144 | Key formulas: 145 | 146 | 1. Energy: $E = mc^2$ 147 | 2. Force: $F = ma$ 148 | 3. Momentum: 149 | 150 | $$ 151 | p = mv 152 | $$ 153 | 154 | 4. Power: \(P = W / t\) 155 | . 156 | !!! info 157 | 158 | Key formulas: 159 | 160 | 1. Energy: $E = mc^2$ 161 | 162 | 1. Force: $F = ma$ 163 | 164 | 1. Momentum: 165 | 166 | $$ 167 | p = mv 168 | $$ 169 | 170 | 1. Power: \(P = W / t\) 171 | . 172 | 173 | Math with Abbreviations 174 | . 175 | *[HTML]: Hyper Text Markup Language 176 | 177 | The HTML specification uses math like $x^2$ occasionally. 178 | 179 | *[LaTeX]: Lamport TeX 180 | 181 | LaTeX is great for typesetting \(\int_0^1 f(x) dx\). 182 | . 183 | *[HTML]: Hyper Text Markup Language 184 | 185 | The HTML specification uses math like $x^2$ occasionally. 186 | 187 | *[LaTeX]: Lamport TeX 188 | 189 | LaTeX is great for typesetting \(\int_0^1 f(x) dx\). 190 | . 191 | 192 | Math in Mixed MkDocs Features 193 | . 194 | !!! example "Combining Features" 195 | Consider the definition: 196 | 197 | Energy 198 | : Defined as $E = mc^2$ 199 | 200 | With tabs: 201 | 202 | === "General Form" 203 | $$ 204 | E = mc^2 205 | $$ 206 | 207 | === "Expanded Form" 208 | Where: 209 | 210 | - $E$ is energy 211 | - $m$ is mass 212 | - $c$ is speed of light 213 | 214 | \[ 215 | c = 3 \times 10^8 m/s 216 | \] 217 | . 218 | !!! example "Combining Features" 219 | 220 | Consider the definition: 221 | 222 | Energy 223 | : Defined as $E = mc^2$ 224 | 225 | With tabs: 226 | 227 | === "General Form" 228 | 229 | $$ 230 | E = mc^2 231 | $$ 232 | 233 | === "Expanded Form" 234 | 235 | Where: 236 | 237 | - $E$ is energy 238 | - $m$ is mass 239 | - $c$ is speed of light 240 | 241 | \[ 242 | c = 3 \times 10^8 m/s 243 | \] 244 | . 245 | -------------------------------------------------------------------------------- /tests/format/fixtures/mkdocstrings_autorefs.md: -------------------------------------------------------------------------------- 1 | heading aliases 2 | . 3 | [](){#contributing} 4 | [](){#development-setup} 5 | ## How to contribute to the project? 6 | . 7 | [](){#contributing} 8 | [](){#development-setup} 9 | ## How to contribute to the project? 10 | . 11 | 12 | Broken Anchor links (https://github.com/KyleKing/mdformat-mkdocs/issues/25) 13 | . 14 | [](<>){#some-anchor-name} 15 | . 16 | [](){#some-anchor-name} 17 | . 18 | -------------------------------------------------------------------------------- /tests/format/fixtures/parsed_result.md: -------------------------------------------------------------------------------- 1 | Dashed list 2 | . 3 | - item 1 4 | - item 2 5 | . 6 | . 7 | 8 | 9 | Numbered list 10 | . 11 | 1. item 1 12 | 1. item 2-a 13 | 1. item 3-a 14 | 2. item 2-b 15 | 1. item 3-b 16 | 2. item 3-b 17 | . 18 | . 19 | 20 | 21 | Combination list 22 | . 23 | - item 1 24 | * item 2 25 | 1. item 3 26 | . 27 | . 28 | 29 | 30 | Corrected Indentation from 3x 31 | . 32 | - item 1 33 | - item 2 34 | - item 3 35 | - item 4 36 | . 37 | . 38 | 39 | Handle Jagged Indents 2x 40 | . 41 | - item 1 42 | - item 2 43 | - item 3 44 | - item 4 45 | - item 5 46 | - item 6 47 | - item 7 48 | - item 8 49 | . 50 | . 51 | 52 | 53 | Nested Python Classes. Resolves #13: https://github.com/KyleKing/mdformat-mkdocs/issues/13 54 | . 55 | 1. Add a serializer class 56 | 57 | ```python 58 | class RecurringEventSerializer(serializers.ModelSerializer): # (1)! 59 | """Used to retrieve recurring_event info""" 60 | 61 | class Meta: 62 | model = RecurringEvent # (2)! 63 | ``` 64 | . 65 | . 66 | 67 | 68 | Deterministic HTML Formatting 69 | . 70 | ??? info "Full-size Image" 71 | There are no additional steps required if keeping full size image. 72 | 73 | <figure markdown> 74 | ![Example Full size Isolated Object Image Black Background](https://github.com/ultralytics/ultralytics/assets/62214284/845c00d0-52a6-4b1e-8010-4ba73e011b99){ width=240 } 75 | <figcaption>Example full-size output</figcaption> 76 | </figure> 77 | . 78 | . 79 | 80 | 81 | Correctly identifies peers when numbering 82 | . 83 | 1. One 84 | 1. 1-A 85 | 2. Two 86 | 1. 2-A 87 | 2. 2-B 88 | . 89 | . 90 | 91 | Do not format code (https://github.com/KyleKing/mdformat-mkdocs/issues/36). Also tested in `test_wrap` for resulting format 92 | . 93 | # A B C 94 | 95 | 1. Create a `.pre-commit-config.yaml` file in your repository and add the desired 96 | hooks. For example: 97 | 98 | ```yaml 99 | repos: 100 | - repo: https://github.com/psf/black 101 | rev: v24.4 102 | 103 | ``` 104 | 105 | ```md 106 | # Title 107 | Content 108 | 1. Numbered List 109 | * Unordered Sub-List 110 | ``` 111 | . 112 | . 113 | 114 | Support inline bulleted code (https://github.com/KyleKing/mdformat-mkdocs/issues/40) 115 | . 116 | - ```python 117 | for idx in range(10): 118 | print(idx) 119 | ``` 120 | 121 | 1. ```bash 122 | for match in %(ls); 123 | do echo match; 124 | done 125 | ``` 126 | 127 | - ```powershell 128 | iex (new-object net.webclient).DownloadString('https://get.scoop.sh') 129 | ``` 130 | 131 | ```txt 132 | - First Line 133 | Second Line 134 | ``` 135 | 136 | ```yaml 137 | repos: 138 | - repo: https://github.com/psf/black 139 | rev: v24.4 140 | ``` 141 | . 142 | . 143 | -------------------------------------------------------------------------------- /tests/format/fixtures/pymd_abbreviations.md: -------------------------------------------------------------------------------- 1 | Abbreviations (Similar to footnote, but with `*`) 2 | . 3 | The HTML specification is maintained. 4 | 5 | *[HTML]: Hyper Text Markup Language 6 | 7 | Potentially other content 8 | 9 | - Which needs to be formatted 10 | - 2x indent 11 | . 12 | The HTML specification is maintained. 13 | 14 | *[HTML]: Hyper Text Markup Language 15 | 16 | Potentially other content 17 | 18 | - Which needs to be formatted 19 | - 2x indent 20 | . 21 | 22 | 23 | Glossary (Appended without the acronym present) 24 | . 25 | *[HTML]: Hyper Text Markup Language 26 | *[W3C]: World Wide Web Consortium 27 | . 28 | *[HTML]: Hyper Text Markup Language 29 | *[W3C]: World Wide Web Consortium 30 | . 31 | 32 | 33 | Broken (Fixes https://github.com/KyleKing/mdformat-mkdocs/issues/28) 34 | . 35 | The HTML specification is maintained by the W3C. 36 | 37 | \*\[HTML\]: Hyper Text Markup Language 38 | \*\[W3C\]: World Wide Web Consortium 39 | . 40 | The HTML specification is maintained by the W3C. 41 | 42 | *[HTML]: Hyper Text Markup Language 43 | *[W3C]: World Wide Web Consortium 44 | . 45 | 46 | Mixed content 47 | . 48 | \*\[HTML\]: Hyper Text Markup Language 49 | - A list 50 | . 51 | *[HTML]: Hyper Text Markup Language 52 | 53 | - A list 54 | . 55 | -------------------------------------------------------------------------------- /tests/format/fixtures/pymd_arithmatex.md: -------------------------------------------------------------------------------- 1 | PyMdown Extensions Arithmatex (Math Support) 2 | . 3 | # Inline Math 4 | 5 | The equation $E = mc^2$ represents energy-mass equivalence. 6 | 7 | Multiple inline: $x + y = z$ and $a^2 + b^2 = c^2$. 8 | 9 | Parenthesis notation: \(F = ma\) is Newton's second law. 10 | 11 | Not math (smart dollar): I have $3.00 and you have $5.00. 12 | 13 | Complex inline: $\frac{p(y|x)p(x)}{p(y)} = p(x|y)$. 14 | . 15 | # Inline Math 16 | 17 | The equation $E = mc^2$ represents energy-mass equivalence. 18 | 19 | Multiple inline: $x + y = z$ and $a^2 + b^2 = c^2$. 20 | 21 | Parenthesis notation: \(F = ma\) is Newton's second law. 22 | 23 | Not math (smart dollar): I have $3.00 and you have $5.00. 24 | 25 | Complex inline: $\frac{p(y|x)p(x)}{p(y)} = p(x|y)$. 26 | . 27 | 28 | Block Math with Double Dollar 29 | . 30 | The Restricted Boltzmann Machine energy function: 31 | 32 | $$ 33 | E(\mathbf{v}, \mathbf{h}) = -\sum_{i,j}w_{ij}v_i h_j - \sum_i b_i v_i - \sum_j c_j h_j 34 | $$ 35 | 36 | This defines the joint probability distribution. 37 | . 38 | The Restricted Boltzmann Machine energy function: 39 | 40 | $$ 41 | E(\mathbf{v}, \mathbf{h}) = -\sum_{i,j}w_{ij}v_i h_j - \sum_i b_i v_i - \sum_j c_j h_j 42 | $$ 43 | 44 | This defines the joint probability distribution. 45 | . 46 | 47 | Block Math with Square Brackets 48 | . 49 | The conditional probabilities are: 50 | 51 | \[ 52 | p(v_i=1|\mathbf{h}) = \sigma\left(\sum_j w_{ij}h_j + b_i\right) 53 | \] 54 | 55 | Where $\sigma$ is the sigmoid function. 56 | . 57 | The conditional probabilities are: 58 | 59 | \[ 60 | p(v_i=1|\mathbf{h}) = \sigma\left(\sum_j w_{ij}h_j + b_i\right) 61 | \] 62 | 63 | Where $\sigma$ is the sigmoid function. 64 | . 65 | 66 | Block Math with LaTeX Environments - align 67 | . 68 | The forward and backward passes: 69 | 70 | \begin{align} 71 | p(v_i=1|\mathbf{h}) & = \sigma\left(\sum_j w_{ij}h_j + b_i\right) \\ 72 | p(h_j=1|\mathbf{v}) & = \sigma\left(\sum_i w_{ij}v_i + c_j\right) 73 | \end{align} 74 | 75 | These equations describe the Gibbs sampling process. 76 | . 77 | The forward and backward passes: 78 | 79 | \begin{align} 80 | p(v_i=1|\mathbf{h}) & = \sigma\left(\sum_j w_{ij}h_j + b_i\right) \\ 81 | p(h_j=1|\mathbf{v}) & = \sigma\left(\sum_i w_{ij}v_i + c_j\right) 82 | \end{align} 83 | 84 | These equations describe the Gibbs sampling process. 85 | . 86 | 87 | Block Math with LaTeX Environments - equation 88 | . 89 | Einstein's field equations: 90 | 91 | \begin{equation} 92 | R_{\mu\nu} - \frac{1}{2}Rg_{\mu\nu} + \Lambda g_{\mu\nu} = \frac{8\pi G}{c^4}T_{\mu\nu} 93 | \end{equation} 94 | 95 | This is the foundation of general relativity. 96 | . 97 | Einstein's field equations: 98 | 99 | \begin{equation} 100 | R_{\mu\nu} - \frac{1}{2}Rg_{\mu\nu} + \Lambda g_{\mu\nu} = \frac{8\pi G}{c^4}T_{\mu\nu} 101 | \end{equation} 102 | 103 | This is the foundation of general relativity. 104 | . 105 | 106 | Mixed Inline and Block Math 107 | . 108 | For the wave equation $\frac{\partial^2 u}{\partial t^2} = c^2 \nabla^2 u$, the solution in one dimension is: 109 | 110 | $$ 111 | u(x,t) = f(x - ct) + g(x + ct) 112 | $$ 113 | 114 | Where $f$ and $g$ are arbitrary functions determined by initial conditions. 115 | 116 | The dispersion relation \(\omega = ck\) relates frequency and wave number. 117 | . 118 | For the wave equation $\frac{\partial^2 u}{\partial t^2} = c^2 \nabla^2 u$, the solution in one dimension is: 119 | 120 | $$ 121 | u(x,t) = f(x - ct) + g(x + ct) 122 | $$ 123 | 124 | Where $f$ and $g$ are arbitrary functions determined by initial conditions. 125 | 126 | The dispersion relation \(\omega = ck\) relates frequency and wave number. 127 | . 128 | -------------------------------------------------------------------------------- /tests/format/fixtures/pymd_arithmatex_ams_environments.md: -------------------------------------------------------------------------------- 1 | ReLU Function with Mixed Syntax (Issue #45) 2 | . 3 | $$ 4 | ReLU(x) = 5 | \begin{cases} 6 | x &\quad\text{if } x > 0\\\ 7 | 0 &\quad\text{otherwise} 8 | \end{cases} 9 | $$ 10 | 11 | \[ x = \frac{4}{5} \] 12 | 13 | What about inline expressions? $\Delta_{distance}= \text{Speed} \cdot \text{Time}$ 14 | . 15 | $$ 16 | ReLU(x) = 17 | \begin{cases} 18 | x &\quad\text{if } x > 0\\\ 19 | 0 &\quad\text{otherwise} 20 | \end{cases} 21 | $$ 22 | 23 | \[ 24 | x = \frac{4}{5} 25 | \] 26 | 27 | What about inline expressions? $\Delta_{distance}= \text{Speed} \cdot \text{Time}$ 28 | . 29 | 30 | AMS Math - align* (unnumbered) 31 | . 32 | The aligned equations without numbers: 33 | 34 | \begin{align*} 35 | x &= a + b \\ 36 | y &= c + d \\ 37 | z &= e + f 38 | \end{align*} 39 | 40 | These are unnumbered aligned equations. 41 | . 42 | The aligned equations without numbers: 43 | 44 | \begin{align*} 45 | x &= a + b \\ 46 | y &= c + d \\ 47 | z &= e + f 48 | \end{align*} 49 | 50 | These are unnumbered aligned equations. 51 | . 52 | 53 | AMS Math - gather 54 | . 55 | Multiple equations centered: 56 | 57 | \begin{gather} 58 | a = b + c \\ 59 | x = y + z \\ 60 | m = n + p 61 | \end{gather} 62 | 63 | The gather environment centers equations. 64 | . 65 | Multiple equations centered: 66 | 67 | \begin{gather} 68 | a = b + c \\ 69 | x = y + z \\ 70 | m = n + p 71 | \end{gather} 72 | 73 | The gather environment centers equations. 74 | . 75 | 76 | AMS Math - gather* 77 | . 78 | Multiple equations centered without numbers: 79 | 80 | \begin{gather*} 81 | \sin^2 x + \cos^2 x = 1 \\ 82 | e^{i\pi} + 1 = 0 \\ 83 | \nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t} 84 | \end{gather*} 85 | 86 | Unnumbered gathered equations. 87 | . 88 | Multiple equations centered without numbers: 89 | 90 | \begin{gather*} 91 | \sin^2 x + \cos^2 x = 1 \\ 92 | e^{i\pi} + 1 = 0 \\ 93 | \nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t} 94 | \end{gather*} 95 | 96 | Unnumbered gathered equations. 97 | . 98 | 99 | AMS Math - multline 100 | . 101 | Long equation split across multiple lines: 102 | 103 | \begin{multline} 104 | a + b + c + d + e + f \\ 105 | + g + h + i + j + k + l \\ 106 | + m + n + o + p = q 107 | \end{multline} 108 | 109 | The multline environment handles long equations. 110 | . 111 | Long equation split across multiple lines: 112 | 113 | \begin{multline} 114 | a + b + c + d + e + f \\ 115 | + g + h + i + j + k + l \\ 116 | + m + n + o + p = q 117 | \end{multline} 118 | 119 | The multline environment handles long equations. 120 | . 121 | 122 | AMS Math - split 123 | . 124 | Split within equation environment: 125 | 126 | \begin{equation} 127 | \begin{split} 128 | a &= b + c \\ 129 | &= d + e \\ 130 | &= f 131 | \end{split} 132 | \end{equation} 133 | 134 | Split provides alignment within a single equation number. 135 | . 136 | Split within equation environment: 137 | 138 | \begin{equation} 139 | \begin{split} 140 | a &= b + c \\ 141 | &= d + e \\ 142 | &= f 143 | \end{split} 144 | \end{equation} 145 | 146 | Split provides alignment within a single equation number. 147 | . 148 | 149 | AMS Math - cases 150 | . 151 | Piecewise function definition: 152 | 153 | $$ 154 | f(x) = \begin{cases} 155 | x^2 & \text{if } x \geq 0 \\ 156 | -x^2 & \text{if } x < 0 157 | \end{cases} 158 | $$ 159 | 160 | The cases environment is useful for piecewise functions. 161 | . 162 | Piecewise function definition: 163 | 164 | $$ 165 | f(x) = \begin{cases} 166 | x^2 & \text{if } x \geq 0 \\ 167 | -x^2 & \text{if } x < 0 168 | \end{cases} 169 | $$ 170 | 171 | The cases environment is useful for piecewise functions. 172 | . 173 | 174 | AMS Math - matrix 175 | . 176 | Basic matrix: 177 | 178 | $$ 179 | A = \begin{matrix} 180 | a & b \\ 181 | c & d 182 | \end{matrix} 183 | $$ 184 | 185 | Simple matrix without delimiters. 186 | . 187 | Basic matrix: 188 | 189 | $$ 190 | A = \begin{matrix} 191 | a & b \\ 192 | c & d 193 | \end{matrix} 194 | $$ 195 | 196 | Simple matrix without delimiters. 197 | . 198 | 199 | AMS Math - pmatrix (parentheses) 200 | . 201 | Matrix with parentheses: 202 | 203 | $$ 204 | B = \begin{pmatrix} 205 | 1 & 2 & 3 \\ 206 | 4 & 5 & 6 \\ 207 | 7 & 8 & 9 208 | \end{pmatrix} 209 | $$ 210 | 211 | The pmatrix environment adds parentheses. 212 | . 213 | Matrix with parentheses: 214 | 215 | $$ 216 | B = \begin{pmatrix} 217 | 1 & 2 & 3 \\ 218 | 4 & 5 & 6 \\ 219 | 7 & 8 & 9 220 | \end{pmatrix} 221 | $$ 222 | 223 | The pmatrix environment adds parentheses. 224 | . 225 | 226 | AMS Math - bmatrix (brackets) 227 | . 228 | Matrix with brackets: 229 | 230 | $$ 231 | C = \begin{bmatrix} 232 | x & y \\ 233 | z & w 234 | \end{bmatrix} 235 | $$ 236 | 237 | The bmatrix environment adds square brackets. 238 | . 239 | Matrix with brackets: 240 | 241 | $$ 242 | C = \begin{bmatrix} 243 | x & y \\ 244 | z & w 245 | \end{bmatrix} 246 | $$ 247 | 248 | The bmatrix environment adds square brackets. 249 | . 250 | 251 | AMS Math - Bmatrix (braces) 252 | . 253 | Matrix with braces: 254 | 255 | $$ 256 | D = \begin{Bmatrix} 257 | \alpha & \beta \\ 258 | \gamma & \delta 259 | \end{Bmatrix} 260 | $$ 261 | 262 | The Bmatrix environment adds curly braces. 263 | . 264 | Matrix with braces: 265 | 266 | $$ 267 | D = \begin{Bmatrix} 268 | \alpha & \beta \\ 269 | \gamma & \delta 270 | \end{Bmatrix} 271 | $$ 272 | 273 | The Bmatrix environment adds curly braces. 274 | . 275 | 276 | AMS Math - vmatrix (vertical bars) 277 | . 278 | Matrix with vertical bars (determinant): 279 | 280 | $$ 281 | \det(E) = \begin{vmatrix} 282 | a & b \\ 283 | c & d 284 | \end{vmatrix} 285 | $$ 286 | 287 | The vmatrix environment adds vertical bars for determinants. 288 | . 289 | Matrix with vertical bars (determinant): 290 | 291 | $$ 292 | \det(E) = \begin{vmatrix} 293 | a & b \\ 294 | c & d 295 | \end{vmatrix} 296 | $$ 297 | 298 | The vmatrix environment adds vertical bars for determinants. 299 | . 300 | 301 | AMS Math - Vmatrix (double vertical bars) 302 | . 303 | Matrix with double vertical bars: 304 | 305 | $$ 306 | \|F\| = \begin{Vmatrix} 307 | x & y \\ 308 | z & w 309 | \end{Vmatrix} 310 | $$ 311 | 312 | The Vmatrix environment adds double vertical bars. 313 | . 314 | Matrix with double vertical bars: 315 | 316 | $$ 317 | \|F\| = \begin{Vmatrix} 318 | x & y \\ 319 | z & w 320 | \end{Vmatrix} 321 | $$ 322 | 323 | The Vmatrix environment adds double vertical bars. 324 | . 325 | 326 | AMS Math - alignat 327 | . 328 | Alignment at multiple points: 329 | 330 | \begin{alignat}{2} 331 | x &= a &&+ b \\ 332 | y &= c &&+ d \\ 333 | z &= e &&+ f 334 | \end{alignat} 335 | 336 | The alignat environment allows multiple alignment points. 337 | . 338 | Alignment at multiple points: 339 | 340 | \begin{alignat}{2} 341 | x &= a &&+ b \\ 342 | y &= c &&+ d \\ 343 | z &= e &&+ f 344 | \end{alignat} 345 | 346 | The alignat environment allows multiple alignment points. 347 | . 348 | 349 | AMS Math - flalign 350 | . 351 | Full-width alignment: 352 | 353 | \begin{flalign} 354 | x &= a + b \\ 355 | y &= c + d 356 | \end{flalign} 357 | 358 | The flalign environment uses full line width. 359 | . 360 | Full-width alignment: 361 | 362 | \begin{flalign} 363 | x &= a + b \\ 364 | y &= c + d 365 | \end{flalign} 366 | 367 | The flalign environment uses full line width. 368 | . 369 | 370 | AMS Math - eqnarray (legacy, but still supported) 371 | . 372 | Legacy equation array: 373 | 374 | \begin{eqnarray} 375 | a &=& b + c \\ 376 | x &=& y + z 377 | \end{eqnarray} 378 | 379 | The eqnarray environment is legacy but still works. 380 | . 381 | Legacy equation array: 382 | 383 | \begin{eqnarray} 384 | a &=& b + c \\ 385 | x &=& y + z 386 | \end{eqnarray} 387 | 388 | The eqnarray environment is legacy but still works. 389 | . 390 | 391 | Mixed AMS Environments 392 | . 393 | Combining different environments: 394 | 395 | \begin{equation} 396 | A = \begin{pmatrix} 397 | 1 & 2 \\ 398 | 3 & 4 399 | \end{pmatrix} 400 | \end{equation} 401 | 402 | \begin{align} 403 | \det(A) &= \begin{vmatrix} 404 | 1 & 2 \\ 405 | 3 & 4 406 | \end{vmatrix} \\ 407 | &= 1 \cdot 4 - 2 \cdot 3 \\ 408 | &= -2 409 | \end{align} 410 | 411 | Multiple environments working together. 412 | . 413 | Combining different environments: 414 | 415 | \begin{equation} 416 | A = \begin{pmatrix} 417 | 1 & 2 \\ 418 | 3 & 4 419 | \end{pmatrix} 420 | \end{equation} 421 | 422 | \begin{align} 423 | \det(A) &= \begin{vmatrix} 424 | 1 & 2 \\ 425 | 3 & 4 426 | \end{vmatrix} \\ 427 | &= 1 \cdot 4 - 2 \cdot 3 \\ 428 | &= -2 429 | \end{align} 430 | 431 | Multiple environments working together. 432 | . 433 | -------------------------------------------------------------------------------- /tests/format/fixtures/pymd_arithmatex_edge_cases.md: -------------------------------------------------------------------------------- 1 | Escaped Delimiters 2 | . 3 | This is not math: \$escaped\$ and \\(also escaped\\). 4 | 5 | Literal dollar: I paid \$5.00 for \$3.00 worth. 6 | . 7 | This is not math: $escaped$ and \\(also escaped\\). 8 | 9 | Literal dollar: I paid $5.00 for $3.00 worth. 10 | . 11 | 12 | Math in Lists 13 | . 14 | 1. First item with math $x = y$ 15 | 2. Second item with block math: 16 | 17 | $$ 18 | E = mc^2 19 | $$ 20 | 21 | 3. Third item with nested list: 22 | - Nested with $a = b$ 23 | - More nested: 24 | 25 | $$ 26 | F = ma 27 | $$ 28 | . 29 | 1. First item with math $x = y$ 30 | 31 | 1. Second item with block math: 32 | 33 | $$ 34 | E = mc^2 35 | $$ 36 | 37 | 1. Third item with nested list: 38 | 39 | - Nested with $a = b$ 40 | 41 | - More nested: 42 | 43 | $$ 44 | F = ma 45 | $$ 46 | . 47 | 48 | Math in Blockquotes 49 | . 50 | > Einstein said $E = mc^2$ 51 | > 52 | > The full equation: 53 | > 54 | > $$ 55 | > E = mc^2 56 | > $$ 57 | . 58 | > Einstein said $E = mc^2$ 59 | > 60 | > The full equation: 61 | > 62 | > $$ 63 | > > E = mc^2 64 | > > 65 | > $$ 66 | . 67 | 68 | Equation Labels with Square Brackets 69 | . 70 | The Pythagorean theorem: 71 | 72 | \[ 73 | a^2 + b^2 = c^2 74 | \] (eq:pythagoras) 75 | 76 | Reference equation (eq:pythagoras) above. 77 | . 78 | The Pythagorean theorem: 79 | 80 | \[ 81 | a^2 + b^2 = c^2 82 | \] (eq:pythagoras) 83 | 84 | Reference equation (eq:pythagoras) above. 85 | . 86 | 87 | Equation Labels with Dollar Signs 88 | . 89 | Maxwell's equations: 90 | 91 | $$ 92 | \nabla \cdot \mathbf{E} = \frac{\rho}{\epsilon_0} 93 | $$ (eq:gauss) 94 | 95 | See equation (eq:gauss) for Gauss's law. 96 | . 97 | Maxwell's equations: 98 | 99 | $$ 100 | \nabla \cdot \mathbf{E} = \frac{\rho}{\epsilon_0} 101 | $$ (eq:gauss) 102 | 103 | See equation (eq:gauss) for Gauss's law. 104 | . 105 | 106 | Math with Line Breaks 107 | . 108 | $$ 109 | x = a + b + c 110 | + d + e 111 | + f 112 | $$ 113 | . 114 | $$ 115 | x = a + b + c 116 | + d + e 117 | + f 118 | $$ 119 | . 120 | 121 | Math in Tables 122 | . 123 | | Formula | Description | 124 | | ------- | ----------- | 125 | | $E=mc^2$ | Energy-mass | 126 | | $F=ma$ | Force | 127 | | \(p=mv\) | Momentum | 128 | . 129 | | Formula | Description | 130 | | ------- | ----------- | 131 | | $E=mc^2$ | Energy-mass | 132 | | $F=ma$ | Force | 133 | | \(p=mv\) | Momentum | 134 | . 135 | 136 | Math at Line Boundaries 137 | . 138 | $start of line$ and $end of line$ 139 | $x=y$ 140 | \(a=b\) and \(c=d\) 141 | . 142 | $start of line$ and $end of line$ 143 | $x=y$ 144 | \(a=b\) and \(c=d\) 145 | . 146 | 147 | Special Characters in Math 148 | . 149 | Brackets: $[a, b]$ and braces: $\{x \in X\}$ 150 | 151 | Pipes: $|x|$ and backslash: $\backslash$ 152 | 153 | Underscores: $x_1, x_2, \ldots, x_n$ 154 | 155 | Carets: $x^2 + y^2 = z^2$ 156 | . 157 | Brackets: $[a, b]$ and braces: $\{x \in X\}$ 158 | 159 | Pipes: $|x|$ and backslash: $\backslash$ 160 | 161 | Underscores: $x_1, x_2, \ldots, x_n$ 162 | 163 | Carets: $x^2 + y^2 = z^2$ 164 | . 165 | 166 | Adjacent Math Expressions 167 | . 168 | Multiple inline: $a$ $b$ $c$ 169 | 170 | With text between: $x$ and $y$ and $z$ 171 | 172 | Different delimiters: $a$ \(b\) $c$ 173 | . 174 | Multiple inline: $a$ $b$ $c$ 175 | 176 | With text between: $x$ and $y$ and $z$ 177 | 178 | Different delimiters: $a$ \(b\) $c$ 179 | . 180 | 181 | Math with Leading/Trailing Whitespace 182 | . 183 | $$ 184 | x = y 185 | $$ 186 | 187 | \[ 188 | a = b 189 | \] 190 | . 191 | $$ 192 | x = y 193 | $$ 194 | 195 | \[ 196 | a = b 197 | \] 198 | . 199 | 200 | Multiline Inline Math 201 | . 202 | This has inline math $\frac{a}{b}$ in it. 203 | 204 | Complex fraction: $\frac{\frac{a}{b}}{\frac{c}{d}}$ is nested. 205 | . 206 | This has inline math $\frac{a}{b}$ in it. 207 | 208 | Complex fraction: $\frac{\frac{a}{b}}{\frac{c}{d}}$ is nested. 209 | . 210 | 211 | Math in Code Fences (Should Not Be Parsed) 212 | . 213 | ```python 214 | # This should not be parsed as math 215 | cost = $5.00 + $3.00 216 | energy = "E = mc^2" 217 | ``` 218 | 219 | But this should: $E = mc^2$ 220 | . 221 | ```python 222 | # This should not be parsed as math 223 | cost = $5.00 + $3.00 224 | energy = "E = mc^2" 225 | ``` 226 | 227 | But this should: $E = mc^2$ 228 | . 229 | 230 | Mixed Math and Text on Same Line 231 | . 232 | The equation $E = mc^2$ was derived by Einstein in 1905. 233 | 234 | Multiple: $a=1$, $b=2$, and $c=3$. 235 | 236 | Parenthesis: \(x=y\) is equivalent to \(y=x\). 237 | . 238 | The equation $E = mc^2$ was derived by Einstein in 1905. 239 | 240 | Multiple: $a=1$, $b=2$, and $c=3$. 241 | 242 | Parenthesis: \(x=y\) is equivalent to \(y=x\). 243 | . 244 | -------------------------------------------------------------------------------- /tests/format/fixtures/pymd_snippet.md: -------------------------------------------------------------------------------- 1 | pymdown snippets (https://github.com/KyleKing/mdformat-mkdocs/issues/34) 2 | . 3 | # Snippets 4 | 5 | --8<-- "filename.ext" 6 | 7 | --8<-- "; skip.md" 8 | 9 | 10 | --8<-- 11 | filename.md 12 | filename.log 13 | --8<-- 14 | 15 | Content of file A. 16 | 17 | Content of file B. 18 | . 19 | # Snippets 20 | 21 | --8<-- "filename.ext" 22 | 23 | --8<-- "; skip.md" 24 | 25 | --8<-- 26 | filename.md 27 | filename.log 28 | --8<-- 29 | 30 | Content of file A. 31 | 32 | Content of file B. 33 | . 34 | -------------------------------------------------------------------------------- /tests/format/fixtures/python_markdown_attr_list.md: -------------------------------------------------------------------------------- 1 | Examples from https://python-markdown.github.io/extensions/attr_list 2 | . 3 | {: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 } 4 | 5 | \{ not an attribute list, but not escaped because '\' is dropped during read_fixture_file } 6 | 7 | { #someid .someclass somekey='some value' } 8 | 9 | This is a paragraph. 10 | {: #an_id .a_class } 11 | 12 | A setext style header {: #setext} 13 | ================================= 14 | 15 | ### A hash style header ### {: #hash } 16 | 17 | [link](http://example.com){: class="foo bar" title="Some title!" } 18 | . 19 | {: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 } 20 | 21 | { not an attribute list, but not escaped because '' is dropped during read_fixture_file } 22 | 23 | { #someid .someclass somekey='some value' } 24 | 25 | This is a paragraph. 26 | {: #an_id .a_class } 27 | 28 | # A setext style header {: #setext} 29 | 30 | ### A hash style header ### {: #hash } 31 | 32 | [link](http://example.com){: class="foo bar" title="Some title!" } 33 | . 34 | 35 | Example from https://github.com/KyleKing/mdformat-mkdocs/issues/45 and source https://raw.githubusercontent.com/arv-anshul/arv-anshul.github.io/refs/heads/main/docs/index.md 36 | . 37 | <div class="grid cards" markdown> 38 | 39 | <!-- Note:   HTML entities are converted to Unicode by mdformat (core behavior) --> 40 | 41 | [:material-account-box:+ .lg .middle +  **About**  ](about/index.md){ .md-button style="text-align: center; display: block;" } 42 | 43 | [:fontawesome-brands-blogger-b:+ .lg .middle +  **Blogs**  ](blog/index.md){ .md-button style="text-align: center; display: block;" } 44 | 45 | </div> 46 | . 47 | <div class="grid cards" markdown> 48 | 49 | <!-- Note:   HTML entities are converted to Unicode by mdformat (core behavior) --> 50 | 51 | [:material-account-box:+ .lg .middle +  **About**  ](about/index.md){ .md-button style="text-align: center; display: block;" } 52 | 53 | [:fontawesome-brands-blogger-b:+ .lg .middle +  **Blogs**  ](blog/index.md){ .md-button style="text-align: center; display: block;" } 54 | 55 | </div> 56 | . 57 | -------------------------------------------------------------------------------- /tests/format/fixtures/regression.md: -------------------------------------------------------------------------------- 1 | Prevent regression with non-deflists: https://github.com/KyleKing/mdformat-mkdocs/issues/56 2 | . 3 | ::: my_lib.core 4 | . 5 | ::: my_lib.core 6 | . 7 | Inline snippet with newline before closing backtick 8 | . 9 | `--8<-- "somesnippet.sh" 10 | ` 11 | . 12 | `--8<-- "somesnippet.sh"` 13 | . 14 | -------------------------------------------------------------------------------- /tests/format/fixtures/semantic_indent.md: -------------------------------------------------------------------------------- 1 | 2 | Dashed list 3 | . 4 | - item 1 5 | - item 2 6 | . 7 | - item 1 8 | - item 2 9 | . 10 | 11 | Asterisk list 12 | . 13 | * item 1 14 | * item 2 15 | . 16 | - item 1 17 | - item 2 18 | . 19 | 20 | Numbered list 21 | . 22 | 1. item 1 23 | 1. item 2 24 | 2. item 2 25 | 1. item 3 26 | 2. item 3 27 | . 28 | 1. item 1 29 | 1. item 2 30 | 1. item 2 31 | 1. item 3 32 | 1. item 3 33 | . 34 | 35 | Combination list 36 | . 37 | - item 1 38 | * item 2 39 | 1. item 3 40 | . 41 | - item 1 42 | - item 2 43 | 1. item 3 44 | . 45 | 46 | Corrected Indentation from 3x 47 | . 48 | - item 1 49 | - item 2 50 | - item 3 51 | - item 4 52 | . 53 | - item 1 54 | - item 2 55 | - item 3 56 | - item 4 57 | . 58 | 59 | Corrected Indentation from 5x 60 | . 61 | - item 1 62 | - item 2 63 | - item 3 64 | - item 4 65 | . 66 | - item 1 67 | - item 2 68 | - item 3 69 | - item 4 70 | . 71 | 72 | Handle Jagged Indents 2x 73 | . 74 | - item 1 75 | - item 2 76 | - item 3 77 | - item 4 78 | - item 5 79 | - item 6 80 | - item 7 81 | - item 8 82 | . 83 | - item 1 84 | - item 2 85 | - item 3 86 | - item 4 87 | - item 5 88 | - item 6 89 | - item 7 90 | - item 8 91 | . 92 | 93 | Handle Jagged Indents 5x 94 | . 95 | - item 1 96 | - item 2 97 | - item 3 98 | - item 4 99 | - item 5 100 | - item 6 101 | - item 7 102 | - item 8 103 | . 104 | - item 1 105 | - item 2 106 | - item 3 107 | - item 4 108 | - item 5 109 | - item 6 110 | - item 7 111 | - item 8 112 | . 113 | 114 | Handle Mixed Indents 115 | . 116 | - item 1 117 | - item 2 118 | - item 3 119 | - item 4 120 | - item 5 121 | - item 6 122 | - item 7 123 | - item 8 124 | . 125 | - item 1 126 | - item 2 127 | - item 3 128 | - item 4 129 | - item 5 130 | - item 6 131 | - item 7 132 | - item 8 133 | . 134 | 135 | List with (what should be converted to a) code block 136 | . 137 | - item 1 138 | 139 | code block 140 | . 141 | - item 1 142 | 143 | code block 144 | . 145 | 146 | List with explicit code block (that should keep indentation) 147 | . 148 | - item 1 149 | 150 | ```txt 151 | code block 152 | ``` 153 | . 154 | - item 1 155 | 156 | ```txt 157 | code block 158 | ``` 159 | . 160 | 161 | 162 | Hanging List (https://github.com/executablebooks/mdformat/issues/371 and https://github.com/KyleKing/mdformat-mkdocs/issues/4) 163 | . 164 | 1. Here indent width is 165 | three. 166 | 167 | 2. Here indent width is 168 | three. 169 | 170 | 123. Here indent width is 171 | five. It needs to be so, because 172 | 173 | Otherwise this next paragraph doesn't belong in the same list item. 174 | . 175 | 1. Here indent width is 176 | three. 177 | 178 | 1. Here indent width is 179 | three. 180 | 181 | 1. Here indent width is 182 | five. It needs to be so, because 183 | 184 | Otherwise this next paragraph doesn't belong in the same list item. 185 | . 186 | 187 | 188 | Code block in semantic indent (https://github.com/KyleKing/mdformat-mkdocs/issues/6) 189 | . 190 | 1. Item 1 191 | with a semantic line feed 192 | 193 | ```bash 194 | echo "I get moved around by prettier/mdformat, originally I am 3 spaces deep" 195 | ``` 196 | 197 | 1. Item 2 198 | 1. Item 3 199 | . 200 | 1. Item 1 201 | with a semantic line feed 202 | 203 | ```bash 204 | echo "I get moved around by prettier/mdformat, originally I am 3 spaces deep" 205 | ``` 206 | 207 | 1. Item 2 208 | 209 | 1. Item 3 210 | . 211 | 212 | 213 | Nested semantic lines (https://github.com/KyleKing/mdformat-mkdocs/issues/7) 214 | . 215 | 1. Line 216 | semantic line 1 (3 spaces deep) 217 | - Bullet (4 spaces deep) 218 | semantic line 2 (6 spaces deep) 219 | . 220 | 1. Line 221 | semantic line 1 (3 spaces deep) 222 | - Bullet (4 spaces deep) 223 | semantic line 2 (6 spaces deep) 224 | . 225 | 226 | 227 | Bulleted semantic lines (https://github.com/KyleKing/mdformat-mkdocs/issues/7) 228 | . 229 | - Line 230 | semantic line 1 (2 spaces deep) 231 | - Bullet (4 spaces deep) 232 | semantic line 2 (6 spaces deep) 233 | . 234 | - Line 235 | semantic line 1 (2 spaces deep) 236 | - Bullet (4 spaces deep) 237 | semantic line 2 (6 spaces deep) 238 | . 239 | 240 | 241 | Nested semantic lines (https://github.com/KyleKing/mdformat-mkdocs/issues/7) 242 | . 243 | - Line 244 | semantic line 1 (2 spaces deep) 245 | 1. Bullet (4 spaces deep) 246 | semantic line 2 (7 spaces deep) 247 | . 248 | - Line 249 | semantic line 1 (2 spaces deep) 250 | 1. Bullet (4 spaces deep) 251 | semantic line 2 (7 spaces deep) 252 | . 253 | 254 | 255 | Table (squished by mdformat>=0.7.19) 256 | . 257 | | Label | Rating | Comment | 258 | |:---------------|---------:|:---------------------| 259 | | Name | 2| <!-- Comment --> | 260 | . 261 | | Label | Rating | Comment | 262 | |:---------------|---------:|:---------------------| 263 | | Name | 2| <!-- Comment --> | 264 | . 265 | 266 | Floating Link 267 | . 268 | > Based on [External Link] 269 | 270 | [external link]: https://github.com/czuli/github-markdown-example/tree/7326f19c94be992319394e5bfeaa07b30f858e46 271 | . 272 | > Based on [External Link] 273 | 274 | [external link]: https://github.com/czuli/github-markdown-example/tree/7326f19c94be992319394e5bfeaa07b30f858e46 275 | . 276 | 277 | Headings 278 | . 279 | # [h1] The largest heading 280 | 281 | ## [h2] heading 282 | 283 | ### [h3] heading 284 | 285 | #### [h4] heading 286 | 287 | ##### [h5] heading 288 | 289 | ###### [h6] The smallest heading 290 | . 291 | # [h1] The largest heading 292 | 293 | ## [h2] heading 294 | 295 | ### [h3] heading 296 | 297 | #### [h4] heading 298 | 299 | ##### [h5] heading 300 | 301 | ###### [h6] The smallest heading 302 | . 303 | 304 | Task List / Check List 305 | . 306 | - [x] #739 307 | - [ ] Add delight to the experience when all tasks are complete :tada: 308 | . 309 | - [x] #739 310 | - [ ] Add delight to the experience when all tasks are complete :tada: 311 | . 312 | 313 | Footnotes (WARN: escaping is prevented by mdformat-gfm. Tested by py#-hook) 314 | . 315 | Here is a simple footnote[^1]. 316 | 317 | You can also use words, to fit your writing style more closely[^note]. 318 | 319 | [^1]: My reference. 320 | [^note]: Named footnotes will still render with numbers instead of the text but allow easier identification and linking.\ 321 | This footnote also has been made with a different syntax using 4 spaces for new lines. 322 | . 323 | Here is a simple footnote[^1]. 324 | 325 | You can also use words, to fit your writing style more closely[^note]. 326 | 327 | \[^1\]: My reference. 328 | \[^note\]: Named footnotes will still render with numbers instead of the text but allow easier identification and linking.\ 329 | This footnote also has been made with a different syntax using 4 spaces for new lines. 330 | . 331 | -------------------------------------------------------------------------------- /tests/format/test_align_semantic_breaks_in_lists.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import mdformat 4 | import pytest 5 | from markdown_it.utils import read_fixture_file 6 | 7 | from tests.helpers import print_text 8 | 9 | FIXTURE_PATH = Path(__file__).parent / "fixtures/semantic_indent.md" 10 | fixtures = read_fixture_file(FIXTURE_PATH) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ("line", "title", "text", "expected"), 15 | fixtures, 16 | ids=[f[1] for f in fixtures], 17 | ) 18 | def test_align_semantic_breaks_in_lists(line, title, text, expected): 19 | output = mdformat.text( 20 | text, 21 | options={"align_semantic_breaks_in_lists": True, "wrap": "keep"}, 22 | extensions={"mkdocs"}, 23 | ) 24 | print_text(output, expected) 25 | assert output == expected 26 | -------------------------------------------------------------------------------- /tests/format/test_format.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import chain 4 | from pathlib import Path 5 | from typing import TypeVar 6 | 7 | import mdformat 8 | import pytest 9 | from markdown_it.utils import read_fixture_file 10 | 11 | from tests.helpers import print_text 12 | 13 | T = TypeVar("T") 14 | 15 | 16 | def flatten(nested_list: list[list[T]]) -> list[T]: 17 | return [*chain(*nested_list)] 18 | 19 | 20 | fixtures = flatten( 21 | [ 22 | read_fixture_file(Path(__file__).parent / "fixtures" / fixture_path) 23 | for fixture_path in ( 24 | "material_content_tabs.md", 25 | "material_deflist.md", 26 | "material_math.md", 27 | "math_with_mkdocs_features.md", 28 | "mkdocstrings_autorefs.md", 29 | "pymd_abbreviations.md", 30 | "pymd_arithmatex.md", 31 | "pymd_arithmatex_ams_environments.md", 32 | "pymd_arithmatex_edge_cases.md", 33 | "pymd_snippet.md", 34 | "python_markdown_attr_list.md", 35 | "regression.md", 36 | "text.md", 37 | ) 38 | ], 39 | ) 40 | 41 | 42 | @pytest.mark.parametrize( 43 | ("line", "title", "text", "expected"), 44 | fixtures, 45 | ids=[f[1] for f in fixtures], 46 | ) 47 | def test_format_fixtures(line, title, text, expected): 48 | output = mdformat.text(text, extensions={"mkdocs"}) 49 | print_text(output, expected) 50 | assert output.rstrip() == expected.rstrip() 51 | -------------------------------------------------------------------------------- /tests/format/test_ignore_missing_references.py: -------------------------------------------------------------------------------- 1 | import mdformat 2 | import pytest 3 | 4 | from tests.helpers import print_text 5 | 6 | TICKET_019 = """Example python mkdocstring snippets 7 | 8 | [package.module.object][] 9 | [Object][package.module.object] 10 | 11 | - [package.module.object][] 12 | - [Object][package.module.object] 13 | """ 14 | 15 | 16 | @pytest.mark.parametrize( 17 | ("text", "expected"), 18 | [ 19 | (TICKET_019, TICKET_019), 20 | ], 21 | ids=["TICKET_019"], 22 | ) 23 | def test_align_semantic_breaks_in_lists(text, expected): 24 | output = mdformat.text( 25 | text, 26 | options={"ignore_missing_references": True}, 27 | extensions={"mkdocs"}, 28 | ) 29 | print_text(output, expected) 30 | assert output == expected 31 | -------------------------------------------------------------------------------- /tests/format/test_number.py: -------------------------------------------------------------------------------- 1 | import mdformat 2 | import pytest 3 | 4 | from tests.helpers import print_text 5 | 6 | CASE_0 = """ 7 | 0. One 8 | 1. AAA 9 | 1. BBB 10 | 1. CCC 11 | 0. Two 12 | 0. Three 13 | 0. Four 14 | 1. AAA 15 | 1. BBB 16 | 1. CCC 17 | 0. Five 18 | 0. Six 19 | 1. AAA 20 | 1. BBB 21 | 1. CCC 22 | 1. aaa 23 | 1. bbb 24 | 1. ccc 25 | 1. ddd 26 | 0. Seven 27 | """ 28 | 29 | 30 | CASE_0_NUMBERED = """ 31 | 0. One 32 | 1. AAA 33 | 2. BBB 34 | 3. CCC 35 | 1. Two 36 | 2. Three 37 | 3. Four 38 | 1. AAA 39 | 2. BBB 40 | 3. CCC 41 | 4. Five 42 | 5. Six 43 | 1. AAA 44 | 2. BBB 45 | 3. CCC 46 | 1. aaa 47 | 2. bbb 48 | 3. ccc 49 | 4. ddd 50 | 6. Seven 51 | """ 52 | 53 | 54 | CASE_1 = """ 55 | 1. One 56 | 1. AAA 57 | 1. BBB 58 | 1. CCC 59 | 1. Two 60 | 1. Three 61 | 1. Four 62 | 1. AAA 63 | 1. BBB 64 | 1. CCC 65 | 1. Five 66 | 1. Six 67 | 1. AAA 68 | 1. BBB 69 | 1. CCC 70 | 1. aaa 71 | 1. bbb 72 | 1. ccc 73 | 1. ddd 74 | 1. Seven 75 | """ 76 | 77 | 78 | CASE_1_NUMBERED = """ 79 | 1. One 80 | 1. AAA 81 | 2. BBB 82 | 3. CCC 83 | 2. Two 84 | 3. Three 85 | 4. Four 86 | 1. AAA 87 | 2. BBB 88 | 3. CCC 89 | 5. Five 90 | 6. Six 91 | 1. AAA 92 | 2. BBB 93 | 3. CCC 94 | 1. aaa 95 | 2. bbb 96 | 3. ccc 97 | 4. ddd 98 | 7. Seven 99 | """ 100 | 101 | 102 | @pytest.mark.parametrize( 103 | ("text", "expected"), 104 | [ 105 | (CASE_0, CASE_0_NUMBERED), 106 | (CASE_1, CASE_1_NUMBERED), 107 | ], 108 | ids=[ 109 | "CASE_0", 110 | "CASE_1", 111 | ], 112 | ) 113 | def test_number(text: str, expected: str): 114 | """Test CLI argument for ordered lists, `--number`.""" 115 | # Check that when --number is set, ordered lists are incremented 116 | output_numbered = mdformat.text( 117 | text, 118 | options={"number": True}, 119 | extensions={"mkdocs"}, 120 | ) 121 | print_text(output_numbered, expected) 122 | assert output_numbered.strip() == expected.strip() 123 | 124 | # Check when not set that ordered lists use a constant 0 or 1 125 | output = mdformat.text(text, extensions={"mkdocs"}) 126 | print_text(output, text) 127 | assert output.strip() == text.strip() 128 | -------------------------------------------------------------------------------- /tests/format/test_parsed_result.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from markdown_it.utils import read_fixture_file 5 | 6 | from mdformat_mkdocs._normalize_list import parse_text 7 | 8 | FIXTURE_PATH = Path(__file__).parent / "fixtures/parsed_result.md" 9 | fixtures = read_fixture_file(FIXTURE_PATH) 10 | 11 | 12 | @pytest.mark.parametrize( 13 | ("line", "title", "text", "expected"), 14 | fixtures, 15 | ids=[f[1] for f in fixtures], 16 | ) 17 | def test_parsed_result(line, title, text, expected, snapshot): 18 | output = parse_text(text=text, inc_numbers=False, use_sem_break=True) 19 | assert output == snapshot 20 | -------------------------------------------------------------------------------- /tests/format/test_tabbed_code_block.py: -------------------------------------------------------------------------------- 1 | import mdformat 2 | import pytest 3 | 4 | from tests.helpers import print_text 5 | 6 | TABBED_CODE_BLOCK = ''' 7 | 1. Add a serializer class 8 | 9 | ```python 10 | class RecurringEventSerializer(serializers.ModelSerializer): # (1)! 11 | \t"""Used to retrieve recurring_event info""" 12 | 13 | \tclass Meta: 14 | \t\tmodel = RecurringEvent # (2)! 15 | ``` 16 | ''' 17 | 18 | 19 | @pytest.mark.parametrize( 20 | ("text", "expected"), 21 | [ 22 | (TABBED_CODE_BLOCK, TABBED_CODE_BLOCK), 23 | ], 24 | ids=["TABBED_CODE_BLOCK"], 25 | ) 26 | def test_tabbed_code_block(text: str, expected: str): 27 | output = mdformat.text(text, extensions={"mkdocs"}) 28 | print_text(output, expected) 29 | assert output.strip() == expected.strip() 30 | -------------------------------------------------------------------------------- /tests/format/test_wrap.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import mdformat 4 | import pytest 5 | 6 | from tests.helpers import print_text 7 | 8 | # FYI: indented text that starts with a number is parsed as the start of a numbered list 9 | 10 | CASE_1 = """ 11 | # Content 12 | 13 | - Test Testing Test Testing Test Testing Test Testing Test Testing 14 | Test Testing 15 | - Test Testing Test Testing Test Testing Test Testing Test Testing 16 | Test Testing Test Testing Test Testing Test Testing Test Testing 17 | Test Testing 18 | 19 | 1. Test Testing Test Testing Test Testing Test Testing Test Testing 20 | Test Testing 21 | 1. Test Testing Test Testing Test Testing Test Testing Test Testing 22 | Test Testing Test Testing Test Testing Test Testing Test Testing 23 | Test Testing 24 | """ 25 | 26 | CASE_1_FALSE_40 = """ 27 | # Content 28 | 29 | - Test Testing Test Testing Test Testing 30 | Test Testing Test Testing Test 31 | Testing 32 | - Test Testing Test Testing Test 33 | Testing Test Testing Test Testing 34 | Test Testing Test Testing Test 35 | Testing Test Testing Test Testing 36 | Test Testing 37 | 38 | 1. Test Testing Test Testing Test 39 | Testing Test Testing Test Testing 40 | Test Testing 41 | 1. Test Testing Test Testing Test 42 | Testing Test Testing Test Testing 43 | Test Testing Test Testing Test 44 | Testing Test Testing Test Testing 45 | Test Testing 46 | """ 47 | 48 | CASE_1_FALSE_80 = """ 49 | # Content 50 | 51 | - Test Testing Test Testing Test Testing Test Testing Test Testing Test Testing 52 | - Test Testing Test Testing Test Testing Test Testing Test Testing Test 53 | Testing Test Testing Test Testing Test Testing Test Testing Test Testing 54 | 55 | 1. Test Testing Test Testing Test Testing Test Testing Test Testing Test Testing 56 | 1. Test Testing Test Testing Test Testing Test Testing Test Testing Test Testing 57 | Test Testing Test Testing Test Testing Test Testing Test Testing 58 | """ 59 | 60 | CASE_1_TRUE_40 = """ 61 | # Content 62 | 63 | - Test Testing Test Testing Test Testing 64 | Test Testing Test Testing Test 65 | Testing 66 | - Test Testing Test Testing Test 67 | Testing Test Testing Test Testing 68 | Test Testing Test Testing Test 69 | Testing Test Testing Test Testing 70 | Test Testing 71 | 72 | 1. Test Testing Test Testing Test 73 | Testing Test Testing Test Testing 74 | Test Testing 75 | 1. Test Testing Test Testing Test 76 | Testing Test Testing Test Testing 77 | Test Testing Test Testing Test 78 | Testing Test Testing Test Testing 79 | Test Testing 80 | """ 81 | 82 | CASE_1_TRUE_80 = """ 83 | # Content 84 | 85 | - Test Testing Test Testing Test Testing Test Testing Test Testing Test Testing 86 | - Test Testing Test Testing Test Testing Test Testing Test Testing Test 87 | Testing Test Testing Test Testing Test Testing Test Testing Test Testing 88 | 89 | 1. Test Testing Test Testing Test Testing Test Testing Test Testing Test Testing 90 | 1. Test Testing Test Testing Test Testing Test Testing Test Testing Test Testing 91 | Test Testing Test Testing Test Testing Test Testing Test Testing 92 | """ 93 | 94 | SPACE = " " 95 | TICKET_020 = f""" 96 | - first line first line first line first line first line first line first line 97 | whitespace{SPACE} 98 | - second line 99 | """ 100 | TICKET_020_TRUE_79 = """ 101 | - first line first line first line first line first line first line first line 102 | whitespace 103 | - second line 104 | """ 105 | 106 | WITH_CODE = """ 107 | # A B C 108 | 109 | 1. Create a `.pre-commit-config.yaml` file in your repository and add the desired 110 | hooks. For example: 111 | 112 | ```yaml 113 | repos: 114 | - repo: https://github.com/psf/black 115 | rev: v24.4 116 | 117 | ``` 118 | 119 | ```md 120 | # Title 121 | Content 122 | 1. Numbered List 123 | * Unordered Sub-List 124 | ``` 125 | """ 126 | WITH_CODE_TRUE_80 = """ 127 | # A B C 128 | 129 | 1. Create a `.pre-commit-config.yaml` file in your repository and add the 130 | desired hooks. For example: 131 | 132 | ```yaml 133 | repos: 134 | - repo: https://github.com/psf/black 135 | rev: v24.4 136 | 137 | ``` 138 | 139 | ```md 140 | # Title 141 | Content 142 | 1. Numbered List 143 | * Unordered Sub-List 144 | ``` 145 | """ 146 | """Do not format code (https://github.com/KyleKing/mdformat-mkdocs/issues/36). 147 | 148 | FYI: See `test_parsed` for debugging internal representation. 149 | 150 | """ 151 | 152 | WITH_ATTR_LIST = r""" 153 | {: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 } 154 | 155 | \\{ not an attribute list and should be wrapped at 80 characters and not kept inline } 156 | 157 | This is a long paragraph that is more than 80 characters long and should be wrapped. 158 | {: #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class } 159 | 160 | A setext style header {: #setext} 161 | ================================= 162 | 163 | ### A hash style header ### {: #hash } 164 | 165 | [link](http://example.com){: class="foo bar" title="Some title!" .a_class1 .a_class2 .a_class1 .a_class2 .a_class1 .a_class2 } 166 | """ 167 | WITH_ATTR_LIST_TRUE_80 = r""" 168 | {: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 } 169 | 170 | \\{ not an attribute list and should be wrapped at 80 characters and not kept 171 | inline } 172 | 173 | This is a long paragraph that is more than 80 characters long and should be 174 | wrapped. 175 | {: #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class #an_id .a_class } 176 | 177 | # A setext style header {: #setext} 178 | 179 | ### A hash style header ### {: #hash } 180 | 181 | [link](http://example.com){: class="foo bar" title="Some title!" .a_class1 .a_class2 .a_class1 .a_class2 .a_class1 .a_class2 } 182 | """ 183 | 184 | CASE_ATTR_LIST_WRAP = """ 185 | This is a paragraph with a long attribute list that should not be wrapped {: .class1 .class2 .class3 .class4 .class5 .class6 .class7 .class8 .class9 .class10 .class11 .class12 .class13 .class14 .class15 .class16 .class17 .class18 .class19 .class20 } 186 | """ 187 | 188 | CASE_ATTR_LIST_WRAP_TRUE_80 = """ 189 | This is a paragraph with a long attribute list that should not be wrapped 190 | {: .class1 .class2 .class3 .class4 .class5 .class6 .class7 .class8 .class9 .class10 .class11 .class12 .class13 .class14 .class15 .class16 .class17 .class18 .class19 .class20 } 191 | """ 192 | 193 | CASE_CAPTION_WRAP = """ 194 | This line is longer than 40 characters and should be wrapped. 195 | 196 | ``` 197 | def gcd(a, b): 198 | if a == 0: return b 199 | elif b == 0: return a 200 | if a > b: return gcd(a % b, b) 201 | else: return gcd(a, b % a) 202 | ``` 203 | 204 | /// caption 205 | Greatest common divisor algorithm. 206 | /// 207 | """ 208 | 209 | CASE_CAPTION_WRAP_TRUE_40 = """ 210 | This line is longer than 40 characters 211 | and should be wrapped. 212 | 213 | ``` 214 | def gcd(a, b): 215 | if a == 0: return b 216 | elif b == 0: return a 217 | if a > b: return gcd(a % b, b) 218 | else: return gcd(a, b % a) 219 | ``` 220 | 221 | /// caption 222 | Greatest common divisor algorithm. 223 | /// 224 | """ 225 | 226 | # Regression test for issue #45 - long links with titles should not break attribute lists 227 | CASE_LONG_LINK_WITH_TITLE = """ 228 | See [https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins](https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins "Code Formatter Plugin Link") for more information about developing code formatter plugins. 229 | """ 230 | 231 | CASE_LONG_LINK_WITH_TITLE_WRAP_80 = """ 232 | See 233 | [https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins](https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins "Code Formatter Plugin Link") 234 | for more information about developing code formatter plugins. 235 | """ 236 | 237 | # Regression test for issue #45 - attribute lists wrap inline with paragraph 238 | CASE_PARAGRAPH_ATTR_LIST = """ 239 | This is a very long paragraph that exceeds the wrapping limit and should be wrapped but the attribute list should stay with the paragraph. 240 | {:.class1 .class2} 241 | """ 242 | 243 | CASE_PARAGRAPH_ATTR_LIST_WRAP_80 = """ 244 | This is a very long paragraph that exceeds the wrapping limit and should be 245 | wrapped but the attribute list should stay with the paragraph. {:.class1 246 | .class2} 247 | """ 248 | 249 | # Regression test for issue #45 - multiple classes in attribute list wrap inline 250 | CASE_MULTICLASS_ATTR_LIST = """ 251 | Short paragraph with many classes below. 252 | {:.class1 .class2 .class3 .class4 .class5 .class6 .class7 .class8 .class9 .class10} 253 | """ 254 | 255 | CASE_MULTICLASS_ATTR_LIST_WRAP_80 = """ 256 | Short paragraph with many classes below. {:.class1 .class2 .class3 .class4 257 | .class5 .class6 .class7 .class8 .class9 .class10} 258 | """ 259 | 260 | # Regression test for issue #45 - link with attribute list 261 | CASE_LINK_ATTR_LIST = """ 262 | [A very long link text that should be wrapped when it exceeds the line limit](http://example.com){: .button .primary .large } 263 | """ 264 | 265 | CASE_LINK_ATTR_LIST_WRAP_80 = """ 266 | [A very long link text that should be wrapped when it exceeds the line limit](http://example.com){: .button .primary .large } 267 | """ 268 | 269 | DEF_LIST_WITH_NESTED_WRAP = dedent( 270 | """\ 271 | term 272 | 273 | : Definition starts with a paragraph, followed by an unordered list: 274 | 275 | - Foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar. 276 | 277 | - (3) bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar. 278 | 279 | - foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar 280 | (split) foo bar foo bar foo bar foo bar. 281 | """, 282 | ) 283 | 284 | DEF_LIST_WITH_NESTED_WRAP_EXPECTED = dedent( 285 | """\ 286 | term 287 | 288 | : Definition starts with a paragraph, followed by an unordered list: 289 | 290 | - Foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar 291 | foo bar foo bar foo bar foo bar. 292 | 293 | - (3) bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar 294 | foo bar foo bar foo bar foo bar. 295 | 296 | - foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo 297 | bar (split) foo bar foo bar foo bar foo bar. 298 | """, 299 | ) 300 | 301 | 302 | @pytest.mark.parametrize( 303 | ("text", "expected", "align_lists", "wrap"), 304 | [ 305 | (CASE_1, CASE_1_FALSE_40, False, 40), 306 | (CASE_1, CASE_1_FALSE_80, False, 80), 307 | (CASE_1, CASE_1_TRUE_40, True, 40), 308 | (CASE_1, CASE_1_TRUE_80, True, 80), 309 | (TICKET_020, TICKET_020_TRUE_79, True, 79), 310 | (WITH_CODE, WITH_CODE_TRUE_80, True, 80), 311 | (WITH_ATTR_LIST, WITH_ATTR_LIST_TRUE_80, True, 80), 312 | (CASE_ATTR_LIST_WRAP, CASE_ATTR_LIST_WRAP_TRUE_80, True, 80), 313 | (CASE_CAPTION_WRAP, CASE_CAPTION_WRAP_TRUE_40, True, 40), 314 | (DEF_LIST_WITH_NESTED_WRAP, DEF_LIST_WITH_NESTED_WRAP_EXPECTED, True, 80), 315 | # Regression tests for issue #45 - attribute lists should not be wrapped 316 | (CASE_LONG_LINK_WITH_TITLE, CASE_LONG_LINK_WITH_TITLE_WRAP_80, True, 80), 317 | (CASE_PARAGRAPH_ATTR_LIST, CASE_PARAGRAPH_ATTR_LIST_WRAP_80, True, 80), 318 | (CASE_MULTICLASS_ATTR_LIST, CASE_MULTICLASS_ATTR_LIST_WRAP_80, True, 80), 319 | (CASE_LINK_ATTR_LIST, CASE_LINK_ATTR_LIST_WRAP_80, True, 80), 320 | ], 321 | ids=[ 322 | "CASE_1_FALSE_40", 323 | "CASE_1_FALSE_80", 324 | "CASE_1_TRUE_40", 325 | "CASE_1_TRUE_80", 326 | "TICKET_020_TRUE_79", 327 | "WITH_CODE_TRUE_80", 328 | "WITH_ATTR_LIST_TRUE_80", 329 | "CASE_ATTR_LIST_WRAP_TRUE_80", 330 | "CASE_CAPTION_WRAP_TRUE_40", 331 | "DEF_LIST_WITH_NESTED_WRAP", 332 | "CASE_LONG_LINK_WITH_TITLE_WRAP_80", 333 | "CASE_PARAGRAPH_ATTR_LIST_WRAP_80", 334 | "CASE_MULTICLASS_ATTR_LIST_WRAP_80", 335 | "CASE_LINK_ATTR_LIST_WRAP_80", 336 | ], 337 | ) 338 | def test_wrap(text: str, expected: str, align_lists: bool, wrap: int): 339 | output = mdformat.text( 340 | text, 341 | options={"align_semantic_breaks_in_lists": align_lists, "wrap": wrap}, 342 | extensions={"mkdocs"}, 343 | ) 344 | print_text(output, expected) 345 | assert output.lstrip() == expected.lstrip() 346 | 347 | 348 | def test_definition_list_wrap_with_gfm(): 349 | output = mdformat.text( 350 | DEF_LIST_WITH_NESTED_WRAP, 351 | options={"wrap": 80}, 352 | extensions={"mkdocs", "gfm"}, 353 | ) 354 | print_text(output, DEF_LIST_WITH_NESTED_WRAP_EXPECTED) 355 | assert output == DEF_LIST_WITH_NESTED_WRAP_EXPECTED 356 | 357 | 358 | def test_definition_list_nested_indentation(): 359 | """Test that nested lists in definition bodies use 4-space increments. 360 | 361 | This is a regression test for issue #63. 362 | """ 363 | input_text = dedent( 364 | """\ 365 | term 366 | 367 | : Definition with a list: 368 | 369 | - First item 370 | - Nested item 371 | - Deep nested item 372 | """, 373 | ) 374 | expected = dedent( 375 | """\ 376 | term 377 | 378 | : Definition with a list: 379 | 380 | - First item 381 | - Nested item 382 | - Deep nested item 383 | """, 384 | ) 385 | output = mdformat.text(input_text, extensions={"mkdocs"}) 386 | print_text(output, expected) 387 | assert output == expected 388 | 389 | # Verify the indentation levels are multiples of 4 390 | lines = output.split("\n") 391 | for line in lines: 392 | if line.strip().startswith("-"): 393 | spaces = len(line) - len(line.lstrip()) 394 | # Spaces before '-' should be 4, 8, or 12 (multiples of 4) 395 | assert spaces in {4, 8, 12}, ( 396 | f"Expected 4/8/12 spaces, got {spaces} in: {line!r}" 397 | ) 398 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """Test Helpers.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | 7 | _SHOW_TEXT = True 8 | 9 | 10 | def separate_indent(line: str) -> tuple[str, str]: 11 | """Separate leading indent from content. Also used by the test suite. 12 | 13 | Returns: 14 | tuple[str, str]: pair of indent and content 15 | 16 | """ 17 | re_indent = re.compile(r"(?P<indent>\s*)(?P<content>[^\s]?.*)") 18 | match = re_indent.match(line) 19 | assert match is not None # for pyright 20 | return (match["indent"], match["content"]) 21 | 22 | 23 | def _print(content: str, show_whitespace: bool) -> None: 24 | for line in content.split("\n"): 25 | indent, content = separate_indent(line) 26 | visible_indents = indent.replace(" ", "→").replace("\t", "➤") 27 | print((visible_indents if show_whitespace else indent) + content) # noqa: T201 28 | 29 | 30 | def print_text(output: str, expected: str, show_whitespace: bool = False) -> None: # noqa: FBT002 31 | """Conditional print text for debugging.""" 32 | if _SHOW_TEXT: 33 | print("-- Output --") # noqa: T201 34 | _print(output, show_whitespace) 35 | print("-- Expected --") # noqa: T201 36 | _print(expected, show_whitespace) 37 | print("-- <End> --") # noqa: T201 38 | -------------------------------------------------------------------------------- /tests/pre-commit-test-align_semantic_breaks_in_lists.md: -------------------------------------------------------------------------------- 1 | # Testing `--align-semantic-breaks-in-lists` 2 | 3 | ## Semantic Line Indents 4 | 5 | See discussion on: https://github.com/KyleKing/mdformat-mkdocs/issues/4 6 | 7 | 1. Here indent width is 8 | three. 9 | 10 | 1. Here indent width is 11 | three. 12 | 13 | 1. Here indent width is 14 | five (three). It needs to be so, because 15 | 16 | Otherwise this next paragraph doesn't belong in the same list item. 17 | 18 | ## Code block in semantic indent 19 | 20 | From: https://github.com/KyleKing/mdformat-mkdocs/issues/6 21 | 22 | 1. Item 1 23 | with a semantic line feed 24 | 25 | ```bash 26 | echo "I get moved around by prettier/mdformat, originally I am 3 spaces deep" 27 | ``` 28 | 29 | 1. Item 2 30 | 31 | 1. Item 3 32 | 33 | ## Nested semantic lines 34 | 35 | From: https://github.com/KyleKing/mdformat-mkdocs/issues/7 36 | 37 | 1. Line 38 | semantic line 1 (3 spaces deep) 39 | - Bullet (4 spaces deep) 40 | semantic line 2 (6 spaces deep) 41 | -------------------------------------------------------------------------------- /tests/pre-commit-test-ignore_missing_references.md: -------------------------------------------------------------------------------- 1 | Testing for <https://github.com/KyleKing/mdformat-mkdocs/issues/19> 2 | 3 | [package.module.object][] 4 | [Object][package.module.object] 5 | 6 | - [package.module.object][] 7 | - [Object][package.module.object] 8 | -------------------------------------------------------------------------------- /tests/pre-commit-test-numbered.md: -------------------------------------------------------------------------------- 1 | # Numbered Lists 2 | 3 | 1. A repository with dependencies and 4 | artifacts is **growing very fast**. 5 | 2\. Indented list item 1 2. Indented 6 | list item 2 7 | 2. If you store artifacts in the 8 | repository, you need to remember to 9 | compile the application before 10 | every commit. 11 | 3. Compiled application. 12 | -------------------------------------------------------------------------------- /tests/pre-commit-test-recommended.md: -------------------------------------------------------------------------------- 1 | # Other Tests 2 | 3 | ## Footnotes 4 | 5 | FYI: Requires `mdformat-footnote`: 6 | 7 | Here is a simple footnote[^1]. 8 | 9 | A footnote can also have multiple lines[^2]. 10 | 11 | You can also use words, to fit your writing style more closely[^note]. 12 | 13 | [^1]: My reference. 14 | [^2]: Every new line should be prefixed with 2 spaces.\ 15 | This allows you to have a footnote with multiple lines. 16 | [^note]: Named footnotes will still render with numbers instead of the text but allow easier identification and linking.\ 17 | This footnote also has been made with a different syntax using 4 spaces for new lines. 18 | -------------------------------------------------------------------------------- /tests/pre-commit-test.md: -------------------------------------------------------------------------------- 1 | # Pre-Commit/Prek Test File 2 | 3 | Testing `mdformat-mkdocs` as a `pre-commit`/`prek` hook (`tox -e py#-hook`) 4 | 5 | # Table 6 | 7 | | Label | Rating | Comment | 8 | | :---- | -----: | :--------------- | 9 | | Name | 2 | <!-- Comment --> | 10 | 11 | ## Floating Link 12 | 13 | > Based on [External Link] 14 | 15 | ______________________________________________________________________ 16 | 17 | ## Arbitrary Markdown thanks to `czuli/github-markdown-example` 18 | 19 | ### **Typo** 20 | 21 | # [h1] The largest heading 22 | 23 | ## [h2] heading 24 | 25 | ### [h3] heading 26 | 27 | #### [h4] heading 28 | 29 | ##### [h5] heading 30 | 31 | ###### [h6] The smallest heading 32 | 33 | ______________________________________________________________________ 34 | 35 | ### Bold 36 | 37 | **This is bold text** 38 | 39 | ______________________________________________________________________ 40 | 41 | ### Italic 42 | 43 | *This text is italicized* 44 | 45 | ______________________________________________________________________ 46 | 47 | ### Strikethrough 48 | 49 | ~~This was mistaken text~~ 50 | 51 | ______________________________________________________________________ 52 | 53 | ### Bold and nested italic 54 | 55 | **This text is _extremely_ important** 56 | 57 | ______________________________________________________________________ 58 | 59 | ### All bold and italic 60 | 61 | ***All this text is important*** 62 | 63 | ______________________________________________________________________ 64 | 65 | ### Subscript 66 | 67 | <sub>This is a subscript text</sub> 68 | 69 | ______________________________________________________________________ 70 | 71 | ### Superscript 72 | 73 | <sup>This is a superscript text</sup> 74 | 75 | ______________________________________________________________________ 76 | 77 | ### Quote 78 | 79 | Text that is not a quote 80 | 81 | > Text that is a quote 82 | > Text that is a quote 83 | > Text that is a quote 84 | 85 | ______________________________________________________________________ 86 | 87 | ### Quoting code 88 | 89 | Use `git status` to list all new or modified files that haven't yet been committed. 90 | 91 | #### Code without highlighting 92 | 93 | Some basic Git commands are: 94 | 95 | ```sh 96 | git status 97 | git add 98 | git commit 99 | ``` 100 | 101 | #### Syntax highlighting 102 | 103 | #### ruby code 104 | 105 | ```ruby 106 | require 'redcarpet' 107 | markdown = Redcarpet.new("Hello World!") 108 | puts markdown.to_html 109 | ``` 110 | 111 | #### bash code 112 | 113 | ```bash 114 | # image 115 | FROM php:7.1-apache 116 | 117 | # envs 118 | ENV INSTALL_DIR /var/www/html 119 | 120 | # install composer 121 | RUN curl -sS https://getcomposer.org/installer | php \ 122 | && mv composer.phar /usr/local/bin/composer 123 | 124 | # install libraries 125 | RUN requirements="cron libpng-dev libmcrypt-dev libmcrypt4 libcurl3-dev libfreetype6 libjpeg62-turbo libjpeg62-turbo-dev libfreetype6-dev libicu-dev libxslt1-dev" \ 126 | && apt-get update \ 127 | && apt-get install -y $requirements \ 128 | && rm -rf /var/lib/apt/lists/* \ 129 | && docker-php-ext-install pdo_mysql \ 130 | && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ 131 | && docker-php-ext-install gd \ 132 | && docker-php-ext-install mcrypt \ 133 | && docker-php-ext-install mbstring \ 134 | && docker-php-ext-install zip \ 135 | && docker-php-ext-install intl \ 136 | && docker-php-ext-install xsl \ 137 | && docker-php-ext-install soap \ 138 | && docker-php-ext-install bcmath 139 | 140 | # add magento cron job 141 | COPY ./crontab /etc/cron.d/magento2-cron 142 | RUN chmod 0644 /etc/cron.d/magento2-cron 143 | RUN crontab -u www-data /etc/cron.d/magento2-cron 144 | 145 | # turn on mod_rewrite 146 | RUN a2enmod rewrite 147 | 148 | # set memory limits 149 | RUN echo "memory_limit=2048M" > /usr/local/etc/php/conf.d/memory-limit.ini 150 | 151 | # clean apt-get 152 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 153 | 154 | # www-data should own /var/www 155 | RUN chown -R www-data:www-data /var/www 156 | 157 | # switch user to www-data 158 | USER www-data 159 | 160 | # copy sources with proper user 161 | COPY --chown=www-data . $INSTALL_DIR 162 | 163 | # set working dir 164 | WORKDIR $INSTALL_DIR 165 | 166 | # composer install 167 | RUN composer install 168 | RUN composer config repositories.magento composer https://repo.magento.com/ 169 | 170 | # chmod directories 171 | RUN chmod u+x bin/magento 172 | 173 | # switch back 174 | USER root 175 | 176 | # run cron alongside apache 177 | CMD [ "sh", "-c", "cron && apache2-foreground" ] 178 | ``` 179 | 180 | ______________________________________________________________________ 181 | 182 | ### Paragraphs 183 | 184 | ### Never store dependencies and compiled artifacts in the repository 185 | 186 | - A repository with dependencies and artifacts is **growing very fast**. Git has not been designed to cope with large files, and the bigger the size of a file, the worse it performs 187 | - If you store artifacts in the repository, you need to remember to compile the application before every commit, so you can commit the altered artifacts together with the changes to the source code. It's very risky because if you **forget to update the artifacts** in the repo, deploying your application to Production server may cause serious problems. 188 | - The tasks that you use to compile, minimize and concatenate files may produce **different results**: it's enough that developers on your team use different versions of Node.js. Committing such files to the repository will incite constant conflicts that need to be solved manually. This makes branch merges very troublesome. 189 | - An application compiled in version X of Node.js may **not work properly** in version Y – yet another human factor issue which makes it difficult to be 100% sure that the generated artifacts are compatible with the Node version on the Production server. 190 | 191 | ### Deploy has more steps 192 | 193 | Okay, so now that we know keeping artifacts and dependencies in the repository is not a good idea, the question is: how *should* we deploy our application to the server? Without a Continuous Deployment tool, it usually looked like this: 194 | 195 | 1. The application is uploaded to the server via SFTP/SCP or Git and built with a script that will download the dependencies and run the tasks directly on the server 196 | 1. In case the SSH access is not available (eg. the server is FTP) the application must be built in a compatible environment before the deployment 197 | 198 | ______________________________________________________________________ 199 | 200 | ### Links 201 | 202 | This site was built using [GitHub Pages](https://pages.github.com/). 203 | 204 | ______________________________________________________________________ 205 | 206 | ### Section links 207 | 208 | [Contribution guidelines for this project](#table) 209 | 210 | ______________________________________________________________________ 211 | 212 | ### Image 213 | 214 | #### image from internet 215 | 216 | ![This is an image](https://buddy.works/assets/svg/brands/buddy.svg) 217 | 218 | #### image from repo with link 219 | 220 | [![](assets/buddy-podcast.png)](https://buddy.works) 221 | 222 | ______________________________________________________________________ 223 | 224 | ### Specifying the theme an image is shown to 225 | 226 | <picture> 227 | <source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/25423296/163456776-7f95b81a-f1ed-45f7-b7ab-8fa810d529fa.png"> 228 | <source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/25423296/163456779-a8556205-d0a5-45e2-ac17-42d089e3c3f8.png"> 229 | <img alt="Shows an illustrated sun in light color mode and a moon with stars in dark color mode." src="https://user-images.githubusercontent.com/25423296/163456779-a8556205-d0a5-45e2-ac17-42d089e3c3f8.png"> 230 | </picture> 231 | 232 | ______________________________________________________________________ 233 | 234 | ### List 235 | 236 | ### Normal list 237 | 238 | - George Washington 239 | - John Adams 240 | - Thomas Jefferson 241 | 242 | ### Ordered list 243 | 244 | 1. James Madison 245 | 1. James Monroe 246 | 1. John Quincy Adams 247 | 248 | ______________________________________________________________________ 249 | 250 | ## TODO List 251 | 252 | - [ ] Task item 253 | - [x] Completed Task item 254 | - [x] Another Completed Task item 255 | - [ ] Task item 256 | - [ ] Task item 257 | - [ ] Task item with code snippet `echo "hello world"` 258 | 259 | ## Mixed List 260 | 261 | 1. Prepare 262 | - Indented item 263 | - Further indented 264 | - [ ] Task 265 | - [ ] [Linked File](./fixtures.md) 266 | 1. Done 267 | 268 | ### Nested Lists 269 | 270 | 1. First list item 271 | - First nested list item 272 | - list item 273 | - list item 274 | - Second nested list item 275 | - list item 276 | - list item 277 | 1. Second list item 278 | - list item 279 | - list item 280 | - list item 281 | 282 | ______________________________________________________________________ 283 | 284 | ### Task lists 285 | 286 | - [x] #739 287 | - [ ] https://github.com/octo-org/octo-repo/issues/740 288 | - [ ] Add delight to the experience when all tasks are complete :tada: 289 | - [ ] (Optional) Open a followup issue 290 | 291 | @github/support What do you think about these updates? 292 | 293 | ______________________________________________________________________ 294 | 295 | ### emoji 296 | 297 | @octocat :+1: This PR looks great - it's ready to merge! :shipit: 298 | 299 | ______________________________________________________________________ 300 | 301 | ### Hiding content with comments 302 | 303 | <!-- This content will not appear in the rendered Markdown --> 304 | 305 | ______________________________________________________________________ 306 | 307 | ### Ignoring Markdown formatting 308 | 309 | Let's rename \*our-new-project\* to \*our-old-project\*. 310 | 311 | ______________________________________________________________________ 312 | 313 | ### Table 314 | 315 | | Left-aligned | Center-aligned | Right-aligned | 316 | | :----------- | :------------: | ------------: | 317 | | git status | git status | git status | 318 | | git diff | git diff | git diff | 319 | 320 | ______________________________________________________________________ 321 | 322 | ### Diagrams 323 | 324 | Here is a simple flow chart: 325 | 326 | ```mermaid 327 | graph TD; 328 | A-->B; 329 | A-->C; 330 | B-->D; 331 | C-->D; 332 | ``` 333 | 334 | ______________________________________________________________________ 335 | 336 | # Deflist Test 337 | 338 | From [`mdformat-deflist`](https://github.com/executablebooks/mdformat-deflist/blob/bbcf9ed4f80847db58b6f932ed95e2c7a6c49ae5/tests/pre-commit-test.md) 339 | 340 | paragraph 341 | 342 | Term 1 343 | 344 | : Definition 1 345 | 346 | Term 2 with *inline markup* 347 | 348 | : Definition 2 349 | 350 | ``` 351 | { some code, part of Definition 2 } 352 | ``` 353 | 354 | Third paragraph of definition 2. 355 | 356 | paragraph 357 | 358 | [external link]: https://github.com/czuli/github-markdown-example/tree/7326f19c94be992319394e5bfeaa07b30f858e46 359 | -------------------------------------------------------------------------------- /tests/render/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/render/fixtures/material_admonitions.md: -------------------------------------------------------------------------------- 1 | Simple admonition 2 | . 3 | !!! note 4 | 5 | *content* 6 | . 7 | <div class="admonition note"> 8 | <p class="admonition-title">Note</p> 9 | <p><em>content</em></p> 10 | </div> 11 | . 12 | 13 | 14 | Simple admonition without title 15 | . 16 | !!! note "" 17 | 18 | *content* 19 | . 20 | <div class="admonition note"> 21 | <p><em>content</em></p> 22 | </div> 23 | . 24 | 25 | 26 | Does not render as admonition 27 | . 28 | !!! 29 | 30 | content 31 | . 32 | <p>!!!</p> 33 | <pre><code>content 34 | </code></pre> 35 | . 36 | 37 | 38 | MKdocs Closed Collapsible Sections 39 | . 40 | ??? note 41 | 42 | content 43 | . 44 | <details class="note"> 45 | <summary>Note</summary> 46 | <p>content</p> 47 | </details> 48 | . 49 | 50 | 51 | MKdocs Open Collapsible Sections 52 | . 53 | ???+ note 54 | 55 | content 56 | . 57 | <details class="note" open="open"> 58 | <summary>Note</summary> 59 | <p>content</p> 60 | </details> 61 | . 62 | -------------------------------------------------------------------------------- /tests/render/fixtures/material_content_tabs.md: -------------------------------------------------------------------------------- 1 | Simple content tab 2 | . 3 | === "CLI" 4 | 5 | ```bash 6 | echo 'args' 7 | ``` 8 | 9 | Regular content 10 | . 11 | <div class="content-tab"> 12 | <p class="content-tab-title">CLI</p> 13 | <pre><code class="language-bash">echo 'args' 14 | </code></pre> 15 | </div> 16 | <p>Regular content</p> 17 | . 18 | 19 | 20 | No vertical separation 21 | . 22 | === "CLI" 23 | ```bash 24 | echo 'args' 25 | ``` 26 | . 27 | <div class="content-tab"> 28 | <p class="content-tab-title">CLI</p> 29 | <pre><code class="language-bash">echo 'args' 30 | </code></pre> 31 | </div> 32 | . 33 | 34 | 35 | Example of non-code content from Material-MkDocs documentation without admonitions 36 | . 37 | === "Unordered list" 38 | 39 | * Sed sagittis eleifend rutrum 40 | * Donec vitae suscipit est 41 | * Nulla tempor lobortis orci 42 | 43 | === "Ordered list" 44 | 45 | 1. Sed sagittis eleifend rutrum 46 | 2. Donec vitae suscipit est 47 | 3. Nulla tempor lobortis orci 48 | . 49 | <div class="content-tab"> 50 | <p class="content-tab-title">Unordered list</p> 51 | <ul> 52 | <li>Sed sagittis eleifend rutrum</li> 53 | <li>Donec vitae suscipit est</li> 54 | <li>Nulla tempor lobortis orci</li> 55 | </ul> 56 | </div> 57 | <div class="content-tab"> 58 | <p class="content-tab-title">Ordered list</p> 59 | <ol> 60 | <li>Sed sagittis eleifend rutrum</li> 61 | <li>Donec vitae suscipit est</li> 62 | <li>Nulla tempor lobortis orci</li> 63 | </ol> 64 | </div> 65 | . 66 | 67 | 68 | Example from Material-MkDocs documentation within an admonition 69 | . 70 | !!! example 71 | 72 | === "Unordered List" 73 | 74 | ```markdown 75 | * Sed sagittis eleifend rutrum 76 | * Donec vitae suscipit est 77 | * Nulla tempor lobortis orci 78 | ``` 79 | 80 | === "Ordered List" 81 | 82 | ```markdown 83 | 1. Sed sagittis eleifend rutrum 84 | 2. Donec vitae suscipit est 85 | 3. Nulla tempor lobortis orci 86 | ``` 87 | . 88 | <div class="admonition example"> 89 | <p class="admonition-title">Example</p> 90 | <div class="content-tab"> 91 | <p class="content-tab-title">Unordered List</p> 92 | <pre><code class="language-markdown">* Sed sagittis eleifend rutrum 93 | * Donec vitae suscipit est 94 | * Nulla tempor lobortis orci 95 | </code></pre> 96 | </div> 97 | <div class="content-tab"> 98 | <p class="content-tab-title">Ordered List</p> 99 | <pre><code class="language-markdown">1. Sed sagittis eleifend rutrum 100 | 2. Donec vitae suscipit est 101 | 3. Nulla tempor lobortis orci 102 | </code></pre> 103 | </div> 104 | </div> 105 | . 106 | 107 | 108 | Support Content Tabs (https://squidfunk.github.io/mkdocs-material/reference/content-tabs/#grouping-code-blocks). Resolves #17: https://github.com/KyleKing/mdformat-admon/issues/17 109 | . 110 | Ultralytics commands use the following syntax: 111 | 112 | !!! Example 113 | 114 | === "CLI" 115 | 116 | ```bash 117 | yolo TASK MODE ARGS 118 | ``` 119 | 120 | === "Python" 121 | 122 | ```python 123 | from ultralytics import YOLO 124 | 125 | # Load a YOLOv8 model from a pre-trained weights file 126 | model = YOLO('yolov8n.pt') 127 | 128 | # Run MODE mode using the custom arguments ARGS (guess TASK) 129 | model.MODE(ARGS) 130 | ``` 131 | . 132 | <p>Ultralytics commands use the following syntax:</p> 133 | <div class="admonition example"> 134 | <p class="admonition-title">Example</p> 135 | <div class="content-tab"> 136 | <p class="content-tab-title">CLI</p> 137 | <pre><code class="language-bash">yolo TASK MODE ARGS 138 | </code></pre> 139 | </div> 140 | <div class="content-tab"> 141 | <p class="content-tab-title">Python</p> 142 | <pre><code class="language-python">from ultralytics import YOLO 143 | 144 | # Load a YOLOv8 model from a pre-trained weights file 145 | model = YOLO('yolov8n.pt') 146 | 147 | # Run MODE mode using the custom arguments ARGS (guess TASK) 148 | model.MODE(ARGS) 149 | </code></pre> 150 | </div> 151 | </div> 152 | . 153 | 154 | 155 | Example from Ultralytics Documentation (https://github.com/ultralytics/ultralytics/blob/0e7221fb62191e18e5ec4f7a9fe6d8927a4446c2/docs/zh/datasets/index.md#L105-L127) 156 | . 157 | ### 优化和压缩数据集的示例代码 158 | 159 | !!! Example "优化和压缩数据集" 160 | 161 | === "Python" 162 | 163 | ```python 164 | from pathlib import Path 165 | from ultralytics.data.utils import compress_one_image 166 | from ultralytics.utils.downloads import zip_directory 167 | 168 | # 定义数据集目录 169 | path = Path('path/to/dataset') 170 | 171 | # 优化数据集中的图像(可选) 172 | for f in path.rglob('*.jpg'): 173 | compress_one_image(f) 174 | 175 | # 将数据集压缩成 'path/to/dataset.zip' 176 | zip_directory(path) 177 | ``` 178 | 179 | 通过遵循这些步骤,您可以贡献一个与 Ultralytics 现有结构良好融合的新数据集。 180 | . 181 | <h3>优化和压缩数据集的示例代码</h3> 182 | <div class="admonition example"> 183 | <p class="admonition-title">优化和压缩数据集</p> 184 | <div class="content-tab"> 185 | <p class="content-tab-title">Python</p> 186 | </div> 187 | <pre><code class="language-python">from pathlib import Path 188 | from ultralytics.data.utils import compress_one_image 189 | from ultralytics.utils.downloads import zip_directory 190 | 191 | # 定义数据集目录 192 | path = Path('path/to/dataset') 193 | 194 | # 优化数据集中的图像(可选) 195 | for f in path.rglob('*.jpg'): 196 | compress_one_image(f) 197 | 198 | # 将数据集压缩成 'path/to/dataset.zip' 199 | zip_directory(path) 200 | </code></pre> 201 | </div> 202 | <p>通过遵循这些步骤,您可以贡献一个与 Ultralytics 现有结构良好融合的新数据集。</p> 203 | . 204 | 205 | 206 | Example from Ultralytics Documentation (https://github.com/ultralytics/ultralytics/blob/fd82a671015a30a869d740c45c65f5633d1d93c4/docs/en/datasets/classify/caltech101.md#L60-L79) 207 | . 208 | ## Citations and Acknowledgments 209 | 210 | If you use the Caltech-101 dataset in your research or development work, please cite the following paper: 211 | 212 | !!! Quote "" 213 | 214 | === "BibTeX" 215 | 216 | ```bibtex 217 | @article{fei2007learning, 218 | title={Learning generative visual models from few training examples: An incremental Bayesian approach tested on 101 object categories}, 219 | author={Fei-Fei, Li and Fergus, Rob and Perona, Pietro}, 220 | journal={Computer vision and Image understanding}, 221 | volume={106}, 222 | number={1}, 223 | pages={59--70}, 224 | year={2007}, 225 | publisher={Elsevier} 226 | } 227 | ``` 228 | . 229 | <h2>Citations and Acknowledgments</h2> 230 | <p>If you use the Caltech-101 dataset in your research or development work, please cite the following paper:</p> 231 | <div class="admonition quote"> 232 | <div class="content-tab"> 233 | <p class="content-tab-title">BibTeX</p> 234 | <pre><code class="language-bibtex">@article{fei2007learning, 235 | title={Learning generative visual models from few training examples: An incremental Bayesian approach tested on 101 object categories}, 236 | author={Fei-Fei, Li and Fergus, Rob and Perona, Pietro}, 237 | journal={Computer vision and Image understanding}, 238 | volume={106}, 239 | number={1}, 240 | pages={59--70}, 241 | year={2007}, 242 | publisher={Elsevier} 243 | } 244 | </code></pre> 245 | </div> 246 | </div> 247 | . 248 | 249 | 250 | . 251 | !!! Example "Update Ultralytics MLflow Settings" 252 | 253 | === "Python" 254 | Within the Python environment, call the `update` method on the `settings` object to change your settings: 255 | ```python 256 | from ultralytics import settings 257 | 258 | # Update a setting 259 | settings.update({'mlflow': True}) 260 | 261 | # Reset settings to default values 262 | settings.reset() 263 | ``` 264 | 265 | === "CLI" 266 | If you prefer using the command-line interface, the following commands will allow you to modify your settings: 267 | ```bash 268 | # Update a setting 269 | yolo settings runs_dir='/path/to/runs' 270 | 271 | # Reset settings to default values 272 | yolo settings reset 273 | ``` 274 | . 275 | <div class="admonition example"> 276 | <p class="admonition-title">Update Ultralytics MLflow Settings</p> 277 | <div class="content-tab"> 278 | <p class="content-tab-title">Python</p> 279 | <p>Within the Python environment, call the <code>update</code> method on the <code>settings</code> object to change your settings:</p> 280 | <pre><code class="language-python">from ultralytics import settings 281 | 282 | # Update a setting 283 | settings.update({'mlflow': True}) 284 | 285 | # Reset settings to default values 286 | settings.reset() 287 | </code></pre> 288 | </div> 289 | <div class="content-tab"> 290 | <p class="content-tab-title">CLI</p> 291 | <p>If you prefer using the command-line interface, the following commands will allow you to modify your settings:</p> 292 | <pre><code class="language-bash"># Update a setting 293 | yolo settings runs_dir='/path/to/runs' 294 | 295 | # Reset settings to default values 296 | yolo settings reset 297 | </code></pre> 298 | </div> 299 | </div> 300 | . 301 | -------------------------------------------------------------------------------- /tests/render/fixtures/material_deflist.md: -------------------------------------------------------------------------------- 1 | Example definition list <https://squidfunk.github.io/mkdocs-material/reference/lists/#using-definition-lists> 2 | . 3 | `Lorem ipsum dolor sit amet` 4 | 5 | : Sed sagittis eleifend rutrum. Donec vitae suscipit est. Nullam tempus 6 | tellus non sem sollicitudin, quis rutrum leo facilisis. 7 | 8 | `Cras arcu libero` 9 | 10 | : Aliquam metus eros, pretium sed nulla venenatis, faucibus auctor ex. Proin 11 | ut eros sed sapien ullamcorper consequat. Nunc ligula ante. 12 | 13 | Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis. 14 | Nam vulputate tincidunt fringilla. 15 | Nullam dignissim ultrices urna non auctor. 16 | . 17 | <dl> 18 | <dt><code>Lorem ipsum dolor sit amet</code></dt> 19 | <dd> 20 | <p>Sed sagittis eleifend rutrum. Donec vitae suscipit est. Nullam tempus 21 | tellus non sem sollicitudin, quis rutrum leo facilisis.</p> 22 | </dd> 23 | <dt><code>Cras arcu libero</code></dt> 24 | <dd> 25 | <p>Aliquam metus eros, pretium sed nulla venenatis, faucibus auctor ex. Proin 26 | ut eros sed sapien ullamcorper consequat. Nunc ligula ante.</p> 27 | <p>Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis. 28 | Nam vulputate tincidunt fringilla. 29 | Nullam dignissim ultrices urna non auctor.</p> 30 | </dd> 31 | </dl> 32 | . 33 | -------------------------------------------------------------------------------- /tests/render/fixtures/mkdocstrings_autorefs.md: -------------------------------------------------------------------------------- 1 | Anchor links (https://github.com/KyleKing/mdformat-mkdocs/issues/25) 2 | . 3 | [](){#some-anchor-name} 4 | . 5 | <p><a id="some-anchor-name" href=""></a></p> 6 | . 7 | 8 | For heading (https://github.com/DFiantHDL/DFHDL/blob/192d7e58e1107da6b0a885e54a853a88bb619f29/docs/user-guide/state/index.md?plain=1#L1-L4 & https://dfianthdl.github.io/user-guide/state/#state-initialization) 9 | . 10 | [](){#state} 11 | # State & Initialization 12 | 13 | Semantically, every DFiant dataflow variable references a token stream (TS). 14 | . 15 | <div> 16 | <a id="state" href=""></a> 17 | <h1>State & Initialization</h1> 18 | </div> 19 | <p>Semantically, every DFiant dataflow variable references a token stream (TS).</p> 20 | . 21 | 22 | Example inline (https://github.com/gustavofoa/dicasdeprogramacao.com.br/blob/e35771b41fce6b7c46875bcf65000f241755c13b/content/posts/iniciante-em-programacao/2013-05-02-operadores-relacionais.md?plain=1#L116C44-L118C16) 23 | . 24 | **maior **e **menor** para 25 | verificar a precedência alfabética de um [](){#spiderWordFound4}texto em 26 | relação a outro 27 | . 28 | <p>**maior **e <strong>menor</strong> para 29 | verificar a precedência alfabética de um <a id="spiderWordFound4" href=""></a>texto em 30 | relação a outro</p> 31 | . 32 | -------------------------------------------------------------------------------- /tests/render/fixtures/mkdocstrings_crossreference.md: -------------------------------------------------------------------------------- 1 | `mkdocstrings` cross-references 2 | . 3 | With a custom title: 4 | [`Object 1`][full.path.object1] 5 | 6 | With the identifier as title: 7 | [full.path.object2][] 8 | . 9 | <p>With a custom title: 10 | <a href="#full.path.object1"><code>Object 1</code></a></p> 11 | <p>With the identifier as title: 12 | <a href="#full.path.object2">full.path.object2</a></p> 13 | . 14 | -------------------------------------------------------------------------------- /tests/render/fixtures/pymd_abbreviations.md: -------------------------------------------------------------------------------- 1 | Abbreviations (Similar to footnote, but with `*`) 2 | . 3 | The HTML specification is maintained by W3C. 4 | 5 | *[HTML]: Hyper Text Markup Language 6 | \*\[W3C\]: World Wide Web Consortium 7 | 8 | Potentially other content 9 | . 10 | <p>The HTML specification is maintained by W3C.</p> 11 | <p>*[HTML]: Hyper Text Markup Language 12 | *[W3C]: World Wide Web Consortium</p> 13 | <p>Potentially other content</p> 14 | . 15 | -------------------------------------------------------------------------------- /tests/render/fixtures/pymd_arithmatex.md: -------------------------------------------------------------------------------- 1 | Inline Math with Dollar Signs 2 | . 3 | The equation $E = mc^2$ is famous. 4 | . 5 | <p>The equation <eq>E = mc^2</eq> is famous.</p> 6 | . 7 | 8 | Inline Math with Parentheses 9 | . 10 | Newton's law \(F = ma\) is fundamental. 11 | . 12 | <p>Newton's law <eq>F = ma</eq> is fundamental.</p> 13 | . 14 | 15 | Block Math with Double Dollars 16 | . 17 | The energy equation: 18 | 19 | $$ 20 | E = mc^2 21 | $$ 22 | . 23 | <p>The energy equation:</p> 24 | <section> 25 | <eqn> 26 | E = mc^2 27 | </eqn> 28 | </section> 29 | . 30 | 31 | Block Math with Square Brackets 32 | . 33 | The force equation: 34 | 35 | \[ 36 | F = ma 37 | \] 38 | . 39 | <p>The force equation:</p> 40 | <section> 41 | <eqn> 42 | F = ma 43 | </eqn> 44 | </section> 45 | . 46 | 47 | Block Math with Equation Label 48 | . 49 | Pythagorean theorem: 50 | 51 | \[ 52 | a^2 + b^2 = c^2 53 | \] (eq:pythagoras) 54 | . 55 | <p>Pythagorean theorem:</p> 56 | <section> 57 | <eqn> 58 | a^2 + b^2 = c^2 59 | </eqn> 60 | </section> 61 | . 62 | 63 | AMS Math - align 64 | . 65 | System of equations: 66 | 67 | \begin{align} 68 | x + y &= 5 \\ 69 | x - y &= 1 70 | \end{align} 71 | . 72 | <p>System of equations:</p> 73 | <div class="math amsmath"> 74 | \begin{align} 75 | x + y &= 5 \\ 76 | x - y &= 1 77 | \end{align} 78 | </div> 79 | . 80 | 81 | AMS Math - equation 82 | . 83 | Einstein's field equation: 84 | 85 | \begin{equation} 86 | R_{\mu\nu} = 0 87 | \end{equation} 88 | . 89 | <p>Einstein's field equation:</p> 90 | <div class="math amsmath"> 91 | \begin{equation} 92 | R_{\mu\nu} = 0 93 | \end{equation} 94 | </div> 95 | . 96 | 97 | Mixed Inline and Block 98 | . 99 | For $x = y$, we have: 100 | 101 | $$ 102 | x + 1 = y + 1 103 | $$ 104 | 105 | Therefore \(x = y\). 106 | . 107 | <p>For <eq>x = y</eq>, we have:</p> 108 | <section> 109 | <eqn> 110 | x + 1 = y + 1 111 | </eqn> 112 | </section> 113 | <p>Therefore <eq>x = y</eq>.</p> 114 | . 115 | -------------------------------------------------------------------------------- /tests/render/fixtures/pymd_captions.md: -------------------------------------------------------------------------------- 1 | pymdown captions (https://github.com/KyleKing/mdformat-mkdocs/issues/51) 2 | . 3 | # Captions 4 | 5 | Captioned content. 6 | 7 | /// caption 8 | First caption. 9 | /// 10 | 11 | /// caption 12 | Second caption. 13 | /// 14 | . 15 | <h1>Captions</h1> 16 | <p>Captioned content.</p> 17 | <figcaption> 18 | <p>First caption.</p> 19 | </figcaption> 20 | <figcaption> 21 | <p>Second caption.</p> 22 | </figcaption> 23 | . 24 | -------------------------------------------------------------------------------- /tests/render/fixtures/pymd_snippet.md: -------------------------------------------------------------------------------- 1 | pymdown snippets (https://github.com/KyleKing/mdformat-mkdocs/issues/34) 2 | . 3 | # Snippets 4 | 5 | --8<-- "filename.ext" 6 | 7 | --8<-- "; skip.md" 8 | 9 | 10 | --8<-- 11 | filename.md 12 | filename.log 13 | --8<-- 14 | 15 | Content of file A. 16 | 17 | Content of file B. 18 | . 19 | <h1>Snippets</h1> 20 | <p>--8<-- "filename.ext"</p> 21 | <p>--8<-- "; skip.md"</p> 22 | <p>--8<-- 23 | filename.md 24 | filename.log 25 | --8<--</p> 26 | <p>Content of file A.</p> 27 | <p>Content of file B.</p> 28 | . 29 | -------------------------------------------------------------------------------- /tests/render/fixtures/python_markdown_attr_list.md: -------------------------------------------------------------------------------- 1 | Examples from https://python-markdown.github.io/extensions/attr_list 2 | <!-- Note: HTML rendering for attribute lists is minimal (formatting only) --> 3 | . 4 | {: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 } 5 | 6 | \{ not an attribute list, but not escaped because '\' is dropped during read_fixture_file } 7 | 8 | { #someid .someclass somekey='some value' } 9 | 10 | This is a paragraph. 11 | {: #an_id .a_class } 12 | 13 | A setext style header {: #setext} 14 | ================================= 15 | 16 | ### A hash style header ### {: #hash } 17 | 18 | [link](http://example.com){: class="foo bar" title="Some title!" } 19 | . 20 | <p><span attributes="['#someid', '.someclass', "somekey='some", "value'", '#id1', '.class1', 'id=id2', 'class="class2', 'class3"', '.class4']">: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 </span></p> 21 | <p>{ not an attribute list, but not escaped because '' is dropped during read_fixture_file }</p> 22 | <p><span attributes="['#someid', '.someclass', "somekey='some", "value'"]"> #someid .someclass somekey='some value' </span></p> 23 | <p>This is a paragraph. 24 | <span attributes="['#an_id', '.a_class']">: #an_id .a_class </span></p> 25 | <h1>A setext style header {: #setext}</h1> 26 | <h3>A hash style header ### <span attributes="['#hash']">: #hash </span></h3> 27 | <p><a href="http://example.com">link</a><span attributes="['class="foo', 'bar"', 'title="Some', 'title!"']">: class="foo bar" title="Some title!" </span></p> 28 | . 29 | 30 | Example from https://github.com/KyleKing/mdformat-mkdocs/issues/45 and source https://raw.githubusercontent.com/arv-anshul/arv-anshul.github.io/refs/heads/main/docs/index.md 31 | . 32 | <div class="grid cards" markdown> 33 | 34 | <!-- Note:   HTML entities are converted to Unicode by mdformat (core behavior) --> 35 | 36 | [:material-account-box:+ .lg .middle +  **About**  ](about/index.md){ .md-button style="text-align: center; display: block;" } 37 | 38 | [:fontawesome-brands-blogger-b:+ .lg .middle +  **Blogs**  ](blog/index.md){ .md-button style="text-align: center; display: block;" } 39 | 40 | </div> 41 | . 42 | <div class="grid cards" markdown> 43 | <!-- Note:   HTML entities are converted to Unicode by mdformat (core behavior) --> 44 | <p><a href="about/index.md">:material-account-box:+ .lg .middle +  <strong>About</strong>  </a><span attributes="['.md-button', 'style="text-align:', 'center;', 'display:', 'block;"']"> .md-button style="text-align: center; display: block;" </span></p> 45 | <p><a href="blog/index.md">:fontawesome-brands-blogger-b:+ .lg .middle +  <strong>Blogs</strong>  </a><span attributes="['.md-button', 'style="text-align:', 'center;', 'display:', 'block;"']"> .md-button style="text-align: center; display: block;" </span></p> 46 | </div> 47 | . 48 | -------------------------------------------------------------------------------- /tests/render/test_render.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from markdown_it import MarkdownIt 5 | from markdown_it.utils import read_fixture_file 6 | 7 | from mdformat_mkdocs.mdit_plugins import ( 8 | material_admon_plugin, 9 | material_content_tabs_plugin, 10 | material_deflist_plugin, 11 | mkdocstrings_autorefs_plugin, 12 | mkdocstrings_crossreference_plugin, 13 | pymd_abbreviations_plugin, 14 | pymd_arithmatex_plugin, 15 | pymd_captions_plugin, 16 | pymd_snippet_plugin, 17 | python_markdown_attr_list_plugin, 18 | ) 19 | from tests.helpers import print_text 20 | 21 | FIXTURE_PATH = Path(__file__).parent / "fixtures" 22 | 23 | 24 | def with_plugin(filename, plugins): 25 | return [(*fix, plugins) for fix in read_fixture_file(FIXTURE_PATH / filename)] 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ("line", "title", "text", "expected", "plugins"), 30 | [ 31 | *with_plugin("material_admonitions.md", [material_admon_plugin]), 32 | *with_plugin( 33 | "material_content_tabs.md", 34 | [material_admon_plugin, material_content_tabs_plugin], 35 | ), 36 | *with_plugin("material_deflist.md", [material_deflist_plugin]), 37 | *with_plugin("mkdocstrings_autorefs.md", [mkdocstrings_autorefs_plugin]), 38 | *with_plugin("pymd_abbreviations.md", [pymd_abbreviations_plugin]), 39 | *with_plugin("pymd_arithmatex.md", [pymd_arithmatex_plugin]), 40 | *with_plugin("pymd_captions.md", [pymd_captions_plugin]), 41 | *with_plugin( 42 | "mkdocstrings_crossreference.md", 43 | [mkdocstrings_crossreference_plugin], 44 | ), 45 | *with_plugin( 46 | "pymd_snippet.md", 47 | [pymd_snippet_plugin], 48 | ), 49 | *with_plugin( 50 | "python_markdown_attr_list.md", 51 | [python_markdown_attr_list_plugin], 52 | ), 53 | ], 54 | ) 55 | def test_render(line, title, text, expected, plugins): 56 | md = MarkdownIt("commonmark") 57 | for plugin in plugins: 58 | md.use(plugin) 59 | if "DISABLE-CODEBLOCKS" in title: 60 | md.disable("code") 61 | md.options["xhtmlOut"] = False 62 | output = md.render(text) 63 | print_text(output, expected, show_whitespace=False) 64 | assert output.rstrip() == expected.rstrip() 65 | -------------------------------------------------------------------------------- /tests/test_mdformat.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import mdformat 4 | 5 | 6 | def test_mdformat_text(): 7 | """Verify that using mdformat works as expected.""" 8 | pth = Path(__file__).parent / "pre-commit-test.md" 9 | content = pth.read_text() 10 | 11 | result = mdformat.text(content, extensions={"mkdocs", "gfm"}) 12 | 13 | pth.write_text(result) # Easier to debug with git 14 | assert result == content, "Differences found in format. Review in git." 15 | --------------------------------------------------------------------------------