pax_global_header00006660000000000000000000000064147501577740014533gustar00rootroot0000000000000052 comment=b087f762b32b9e995083ecfa97008e904f8e6445 mdformat-0.7.22/000077500000000000000000000000001475015777400134345ustar00rootroot00000000000000mdformat-0.7.22/.bumpversion.cfg000066400000000000000000000015711475015777400165500ustar00rootroot00000000000000[bumpversion] commit = True tag = True tag_name = {new_version} current_version = 0.7.22 [bumpversion:file:pyproject.toml] search = version = "{current_version}" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT replace = version = "{new_version}" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT [bumpversion:file:src/mdformat/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bumpversion:file:docs/users/installation_and_usage.md] search = rev: {current_version} # Use the ref you want to point at replace = rev: {new_version} # Use the ref you want to point at [bumpversion:file:docs/conf.py] search = release = '{current_version}' # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT replace = release = '{new_version}' # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT mdformat-0.7.22/.flake8000066400000000000000000000003651475015777400146130ustar00rootroot00000000000000[flake8] max-line-length = 88 max-complexity = 10 extend-ignore = # E203: Whitespace before ':' (violates PEP8 and black style) E203, # A005: A module is shadowing a Python builtin module A005, extend-exclude = */site-packages/* mdformat-0.7.22/.gitattributes000066400000000000000000000001361475015777400163270ustar00rootroot00000000000000# Don't do text manipulations (line ending changes). Tests require # unchanged files. * -text mdformat-0.7.22/.github/000077500000000000000000000000001475015777400147745ustar00rootroot00000000000000mdformat-0.7.22/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475015777400171575ustar00rootroot00000000000000mdformat-0.7.22/.github/ISSUE_TEMPLATE/bug.yaml000066400000000000000000000035031475015777400206210ustar00rootroot00000000000000name: Report a bug 🐛 description: Mdformat crashes or exit code is non-zero, output is invalid CommonMark, rendered document is visually different after formatting... labels: [bug] body: - type: textarea id: describe attributes: label: Describe the bug description: | Provide a short description (one or two sentences) about the problem. What did you expect to happen, and what is actually happening? value: | **context** When I do ___. **expectation** I expected ___ to occur. **bug** But instead ___ happens Here's an error message I ran into... ```console $ mdformat README.md ERROR ... ``` validations: required: true - type: textarea id: reproduce attributes: label: Reproduce the bug description: | Provide information that others may use to re-produce this behavior. For example: - A self-contained command to reproduce the error, e.g. ```console printf -- '- item A\n - item A.a\n- item B' | pipx run mdformat - ``` - Minimal content of a file that when formatted causes the error. Command to format the file. placeholder: | 1. Create a file test.md with the content '...' 2. Run `mdformat test.md` 3. See error validations: required: true - type: textarea id: environment attributes: label: List your environment description: | List the environment needed to reproduce the error. Here are a few ideas: - The output of: ```console mdformat --version ``` - The version of Python you're using. - Your operating system placeholder: | ``` ❯ mdformat --version mdformat 0.7.18 (mdformat_black: 0.1.1) ``` validations: required: true mdformat-0.7.22/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005621475015777400211520ustar00rootroot00000000000000contact_links: - name: Why does Mdformat use backslash to escape my Markdown engine specific syntax? ⁉️ url: https://github.com/hukkin/mdformat?tab=readme-ov-file#why-does-mdformat-backslash-escape-special-syntax-specific-to-mkdocs--hugo--obsidian--github--some-other-markdown-engine about: It's likely you need to install a plugin. See the FAQ more info. mdformat-0.7.22/.github/ISSUE_TEMPLATE/enhancement.yaml000066400000000000000000000012241475015777400223270ustar00rootroot00000000000000name: Request an enhancement 💡 description: Suggest an idea for this project labels: [enhancement] body: - type: textarea id: context attributes: label: Context description: | - Provide background to help others understand this issue. - Describe the problem or need you'd like to address. validations: required: true - type: textarea id: proposal attributes: label: Proposal description: | - A simple and clear description of what you're proposing. - Ideas or constraints for how to implement this proposal - Important considerations to think about or discuss validations: required: false mdformat-0.7.22/.github/workflows/000077500000000000000000000000001475015777400170315ustar00rootroot00000000000000mdformat-0.7.22/.github/workflows/tests.yaml000066400000000000000000000054751475015777400210720ustar00rootroot00000000000000name: Tests on: push: branches: [ master ] tags: [ '[0-9]+.[0-9]+.[0-9]+*' ] pull_request: branches: [ master ] jobs: linters: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Installation (deps and package) run: | pip install . pre-commit mypy==1.13.0 -r tests/requirements.txt - name: run linters run: | mdformat --check docs/ README.md mypy src/ tests/ pre-commit run -a pre-commit try-repo . mdformat --files README.md tests: runs-on: ${{ matrix.os }} strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14-dev'] os: [ubuntu-latest, macos-latest, windows-latest] continue-on-error: ${{ matrix.python-version == '3.14-dev' }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Installation (deps and package) run: | pip install . -r tests/requirements.txt - name: Test with pytest run: | pytest --cov - name: Report coverage if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} # Run mdformat-gfm's tests to catch issues like https://github.com/hukkin/mdformat/issues/501. test-gfm-plugin: runs-on: ubuntu-latest continue-on-error: true steps: - uses: actions/setup-python@v5 with: python-version: '3.x' - uses: actions/checkout@v4 with: path: mdformat/ - uses: actions/checkout@v4 with: repository: hukkin/mdformat-gfm path: mdformat-gfm/ - run: | pip install pytest pip install --editable mdformat-gfm/ pip install --force-reinstall --editable mdformat/ - run: | pytest mdformat-gfm/ allgood: runs-on: ubuntu-latest needs: - linters - tests steps: - run: echo "Great success!" pypi-publish: # Only publish if all other jobs succeed needs: [ allgood ] if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install build and publish tools run: | pip install build twine - name: Build and check run: | rm -rf dist/ && python -m build twine check --strict dist/* - name: Publish run: | twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} mdformat-0.7.22/.gitignore000066400000000000000000000034561475015777400154340ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # IntelliJ .idea/ # VS Code .vscode/ mdformat-0.7.22/.pre-commit-config.yaml000066400000000000000000000030501475015777400177130ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 hooks: - id: check-yaml - id: check-toml - repo: https://github.com/pre-commit/pygrep-hooks rev: 3a6eb0fadf60b3cccfd80bad9dbb6fae7e47b316 # frozen: v1.10.0 hooks: - id: python-use-type-annotations - id: python-check-blanket-noqa - id: python-check-blanket-type-ignore - repo: https://github.com/asottile/yesqa rev: f2ae90cf9e1661ade79d0881186ce4fd7ba6ee79 # frozen: v1.5.0 hooks: - id: yesqa additional_dependencies: - flake8-bugbear - flake8-builtins - flake8-comprehensions - repo: https://github.com/MarcoGorelli/absolufy-imports rev: 1683a63d850e065495c05b771e4a20db010a58e7 # frozen: v0.3.1 hooks: - id: absolufy-imports - repo: https://github.com/PyCQA/isort rev: 0a0b7a830386ba6a31c2ec8316849ae4d1b8240d # frozen: 6.0.0 hooks: - id: isort - repo: https://github.com/psf/black rev: 8a737e727ac5ab2f1d4cf5876720ed276dc8dc4b # frozen: 25.1.0 hooks: - id: black - repo: https://github.com/hukkin/docformatter rev: ab802050e6e96aaaf7f917fcbc333bb74e2e57f7 # frozen: v1.4.2 hooks: - id: docformatter - repo: https://github.com/PyCQA/flake8 rev: e43806be3607110919eff72939fda031776e885a # frozen: 7.1.1 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-builtins - flake8-comprehensions - repo: https://github.com/pre-commit/pre-commit rev: b152e922ef11a97efe22ca7dc4f90011f0d1711c # frozen: v4.1.0 hooks: - id: validate_manifest mdformat-0.7.22/.pre-commit-hooks.yaml000066400000000000000000000002701475015777400175720ustar00rootroot00000000000000- id: mdformat name: mdformat description: "CommonMark compliant Markdown formatter" entry: mdformat language: python types: [markdown] minimum_pre_commit_version: '1.0.0' mdformat-0.7.22/.readthedocs.yaml000066400000000000000000000003041475015777400166600ustar00rootroot00000000000000version: 2 build: os: 'ubuntu-lts-latest' tools: python: '3.13' sphinx: configuration: docs/conf.py fail_on_warning: true python: install: - requirements: docs/requirements.txt mdformat-0.7.22/LICENSE000066400000000000000000000020601475015777400144370ustar00rootroot00000000000000MIT License Copyright (c) 2021 Taneli Hukkinen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mdformat-0.7.22/MANIFEST.in000066400000000000000000000000151475015777400151660ustar00rootroot00000000000000prune tests/ mdformat-0.7.22/README.md000066400000000000000000000205061475015777400147160ustar00rootroot00000000000000
[![Documentation Status](https://readthedocs.org/projects/mdformat/badge/?version=latest)](https://mdformat.readthedocs.io/en/latest/?badge=latest) [![Build Status](https://github.com/hukkin/mdformat/actions/workflows/tests.yaml/badge.svg?branch=master)](https://github.com/hukkin/mdformat/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush) [![codecov.io](https://codecov.io/gh/hukkin/mdformat/branch/master/graph/badge.svg)](https://codecov.io/gh/hukkin/mdformat) [![PyPI version](https://img.shields.io/pypi/v/mdformat)](https://pypi.org/project/mdformat) # ![mdformat](https://raw.githubusercontent.com/hukkin/mdformat/master/docs/_static/logo.svg) > CommonMark compliant Markdown formatter
Mdformat is an opinionated Markdown formatter that can be used to enforce a consistent style in Markdown files. Mdformat is a Unix-style command-line tool as well as a Python library. Find out more in the [docs](https://mdformat.readthedocs.io). ## Installing Install with [CommonMark](https://spec.commonmark.org/current/) support: ```bash pipx install mdformat ``` Install with [GitHub Flavored Markdown (GFM)](https://github.github.com/gfm/) support: ```bash pipx install mdformat pipx inject mdformat mdformat-gfm ``` Note that GitHub's Markdown renderer supports syntax extensions not included in the GFM specification. For full GitHub support do: ```bash pipx install mdformat pipx inject mdformat mdformat-gfm mdformat-frontmatter mdformat-footnote mdformat-gfm-alerts ``` Install with [Markedly Structured Text (MyST)](https://myst-parser.readthedocs.io/en/latest/using/syntax.html) support: ```bash pipx install mdformat pipx inject mdformat mdformat-myst ``` ## Command line usage ### Format files Format files `README.md` and `CHANGELOG.md` in place ```bash mdformat README.md CHANGELOG.md ``` Format `.md` files in current working directory recursively ```bash mdformat . ``` Read Markdown from standard input until `EOF`. Write formatted Markdown to standard output. ```bash mdformat - ``` ### Check formatting ```bash mdformat --check README.md CHANGELOG.md ``` This will not apply any changes to the files. If a file is not properly formatted, the exit code will be non-zero. ### Options ```console foo@bar:~$ mdformat --help usage: mdformat [-h] [--check] [--no-validate] [--version] [--number] [--wrap {keep,no,INTEGER}] [--end-of-line {lf,crlf,keep}] [--exclude PATTERN] [--extensions EXTENSION] [--codeformatters LANGUAGE] [paths ...] CommonMark compliant Markdown formatter positional arguments: paths files to format options: -h, --help show this help message and exit --check do not apply changes to files --no-validate do not validate that the rendered HTML is consistent --version show program's version number and exit --number apply consecutive numbering to ordered lists --wrap {keep,no,INTEGER} paragraph word wrap mode (default: keep) --end-of-line {lf,crlf,keep} output file line ending mode (default: lf) --exclude PATTERN exclude files that match the Unix-style glob pattern (multiple allowed) --extensions EXTENSION require and enable an extension plugin (multiple allowed) (use `--no-extensions` to disable) (default: all enabled) --codeformatters LANGUAGE require and enable a code formatter plugin (multiple allowed) (use `--no-codeformatters` to disable) (default: all enabled) ``` The `--exclude` option is only available on Python 3.13+. ## Documentation This README merely provides a quickstart guide for the command line interface. For more information refer to the [documentation](https://mdformat.readthedocs.io). Here's a few pointers to get you started: - [Style guide](https://mdformat.readthedocs.io/en/stable/users/style.html) - [Python API usage](https://mdformat.readthedocs.io/en/stable/users/installation_and_usage.html#python-api-usage) - [Usage as a pre-commit hook](https://mdformat.readthedocs.io/en/stable/users/installation_and_usage.html#usage-as-a-pre-commit-hook) - [Plugin usage](https://mdformat.readthedocs.io/en/stable/users/plugins.html) - [Plugin development guide](https://mdformat.readthedocs.io/en/stable/contributors/contributing.html) - [List of code block formatter plugins](https://mdformat.readthedocs.io/en/stable/users/plugins.html#existing-plugins) - [List of parser extension plugins](https://mdformat.readthedocs.io/en/stable/users/plugins.html#id1) - [Changelog](https://mdformat.readthedocs.io/en/stable/users/changelog.html) ## Frequently Asked Questions ### Why does mdformat backslash escape special syntax specific to MkDocs / Hugo / Obsidian / GitHub / some other Markdown engine? Mdformat is a CommonMark formatter. It doesn't have out-of-the-box support for syntax other than what is defined in [the CommonMark specification](https://spec.commonmark.org/current/). The custom syntax that these Markdown engines introduce typically redefines the meaning of angle brackets, square brackets, parentheses, hash character — characters that are special in CommonMark. Mdformat often resorts to backslash escaping these characters to ensure its formatting changes never alter a rendered document. Additionally some engines, namely MkDocs, [do not support](https://github.com/mkdocs/mkdocs/issues/1835) CommonMark to begin with, so incompatibilities are unavoidable. Luckily mdformat is extensible by plugins. For many Markdown engines you'll find support by searching [the plugin docs](https://mdformat.readthedocs.io/en/stable/users/plugins.html) or [mdformat GitHub topic](https://github.com/topics/mdformat). You may also want to consider a documentation generator that adheres to CommonMark as its base syntax e.g. [mdBook](https://rust-lang.github.io/mdBook/) or [Sphinx with Markdown](https://www.sphinx-doc.org/en/master/usage/markdown.html). ### Why not use [Prettier](https://github.com/prettier/prettier) instead? Mdformat is pure Python code! Python is pre-installed on macOS and virtually any Linux distribution, meaning that typically little to no additional installations are required to run mdformat. This argument also holds true when using together with [pre-commit](https://github.com/pre-commit/pre-commit) (also Python). Prettier on the other hand requires Node.js/npm. Prettier suffers from [numerous](https://github.com/prettier/prettier/issues?q=is%3Aopen+label%3Alang%3Amarkdown+label%3Atype%3Abug+) bugs, many of which cause changes in Markdown AST and rendered HTML. Many of these bugs are a consequence of using [`remark-parse`](https://github.com/remarkjs/remark/tree/main/packages/remark-parse) v8.x as Markdown parser which, according to the author themselves, is [inferior to markdown-it](https://github.com/remarkjs/remark/issues/75#issuecomment-143532326) used by mdformat. `remark-parse` v9.x is advertised as CommonMark compliant and presumably would fix many of the issues, but is not used by Prettier (v3.3.3) yet. Prettier (v3.3.3), being able to format many languages other than Markdown, is a large package with 73 direct dependencies (mdformat only has one in Python 3.11+). This can be a disadvantage in many environments, one example being size optimized Docker images. Mdformat's parser extension plugin API allows not only customization of the Markdown specification in use, but also advanced features like [automatic table of contents generation](https://github.com/hukkin/mdformat-toc). Also provided is a code formatter plugin API enabling integration of embedded code formatting for any programming language. ### What's wrong with the mdformat logo? It renders incorrectly and is just terrible in general. Nope, the logo is actually pretty great – you're terrible. The logo is more a piece of art than a logo anyways, depicting the horrors of poorly formatted text documents. I made it myself! That said, if you have any graphic design skills and want to contribute a revised version, a PR is more than welcome 😄. mdformat-0.7.22/docs/000077500000000000000000000000001475015777400143645ustar00rootroot00000000000000mdformat-0.7.22/docs/_static/000077500000000000000000000000001475015777400160125ustar00rootroot00000000000000mdformat-0.7.22/docs/_static/logo-150px.png000066400000000000000000000133401475015777400203340ustar00rootroot00000000000000PNG  IHDRr(vsBIT|d pHYsbtEXtSoftwarewww.inkscape.org<]IDATx{UL2HHLkAPDP$JxqOPOW\v> $0T2dH I&3uGGwOO}2uԭݧuwt@0R*dpnܕ5Vd4 ~'/ҹUɍ2|.,VZj'`iMt2UK)JXҊT3 +@)5N>#\Wi%Ê8zʩ3X|zT#ըw"Gv(7^U Uʍf.2UtG?>2oir,ne#)|ŔKp%YNóqoqr+Vn"z9 綸UN /c^+ϕ[Jutρ^' ;=ԫ1T9ɨb,W8kN7uvʩW5bD\5ڌ]i> J킇eT0@RR+2#+ MLszU84P`ktQĚQ8xGM GgSjمQ *y2i(#2; G=zU\xeP#@ȡǗQO`XEv|$j{/ H^^-'`25b k]y6Semd.*b#Hۯdg[2ڨ)^#A=gVT%ʩH"0AJW+,H^SFF a W.7"߭D kȈ]2"kL{&5_;/pYync֔SJ0o|)TʫY kYfˬZY)Y;}bh )65KLkVZҊJ6^f|Ӊ^7t&UZNZufY~u-u:e,7/8j}.2G7'ԱhXmԫU/n` pܕ<< 348y 1]{UX\l-Y41p`$f?º`P8j[lYeX o[JQ`LqVoX䕕Bty<$0"2M=nb8(j>R~NLv +]ǥn}e/KVYx $F B"z1[)~ &P#w{,c$P]^n $E`Y8d)f;:+o*|*7OAjO]'ְ;F2U +WDG>uȫ +UeXyح\Ju#Mm@Fa•w=@:uFar>JB022IZDJ 5x/̐71h-;);Ž>dUk1Qh0,(,XVH?.iFa)ݬgV@4> | /UQaX,oc$Dt+jM"7S@qwC: FmRmGTp1j kPU|iS/,ؙLJ+Q Ta\Kbsv%ocgp+\RUc6)eՠn7"2ebK{dOb=(SգUeXD~Yep{>&gX ȡ€!0V@I +$PV^0U'|Nv u7ɷ2DZx?‰(&!(o`G;\{[EDxF!JVc5/j$'w.ӳ<ߍtGH 5IZ^L+Ƚb=I19T8K}$]EFz]3A=ᨲ'F hóPWj ~|JtM6S ץsZc*TzRs.8GU~ىcwKG=gFVfׄXcX~'#p_fl-t؃ S!+| mUv^[PaI‹ tqtU|͉Y%7?a!ꀓEXq4gÅB%B1Yʗ]|keF(⾾ZyX>Ò9czYTd} Mw `8/+,ZfĞVXf;:A.C3hdKON"?:;{#t%v&>˯gt]SF}7k(8Eԛt.@Ѱ'C4gJˇ['Փ;ڽfDD])mh&xsu$8Gt#]Cv"\ 7P]yJ,z}A|ɒ =hDX]1cAp5TaMmNr#Yx\L T{1$,󅷉ݎrW:;}oB/!=mרptbv`aw?ڣ2÷Tr$Zg& hW͜ +VBK wYi7ie׫=pp<N!I3>XWr |pj 14S:M AM>Q˭Rf3In_]C>ݕYqQr߸N7rf{3o3}\"Ot=F]^!w8~c-"i*9! .wRKF=G$<Z+o;gߥopU!غqu܃1jUڰ6դ 0K~uVf?l-(lg&hd\)Z#nnDsVbBdS8mp1b?>}L V*=25ٴztmk-Ԙ".۩wһx#h㱊 a[-`"Tk& iYlVxZ8 Z7T`jh aGOUʥYe;,C(a)ܗHpEM/^pL} NKlt>JDm'%5?JS"<^3ߴ-q(1M5p*W+Iqgxٕ3]-\BvXBz .z@,7.ǀ3tY$‚$ݧ,ѐ@MRDQjt H0=I9ţ~,.[`³`e2,5̥n]\LcaQhweEٞe*_x|jDy3ZQGpP+F .hsekѧ%v5w]vqtݖf^T6/">ﬡ#$ʺ!!$Mvi:=MP:'wKE6񲌱f,Pf-NUGm~5lUZSEC]OS/hK0^\KwfcN2A4!/=gVM2FpߔXY ˫ewiLa _~(T`%obZa6x#| _\hIݶ1Yf.2oV"&$W2zt70BVJ+GE 2^Exlf$^PNɬfL>O @LRL.p4Yz,!<:)\e?@-sÞ W`46DO!QÌL;PriZg/uxMrIm'//tH}߃)->"W,B*Ѹ+7Kcz`/.=Z{6z >^h>OkqMw#u V?R_SZA`Ycc7`S˞dV3gecIO$]y5EwCX̥ZAЋ2,gxGc@+J+VUZCJOaut̮TUPLDŕ.M+rs5Pi5JAnyZ̵B0S*vpEvhфoiKUr6 image/svg+xml mdformat-0.7.22/docs/conf.py000066400000000000000000000047611475015777400156730ustar00rootroot00000000000000# fmt: off # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'mdformat' copyright = '2021, Taneli Hukkinen' # noqa: A001 author = 'Taneli Hukkinen' # The full version, including alpha/beta/rc tags release = '0.7.22' # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['myst_parser'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'furo' html_logo = "_static/logo-150px.png" html_show_sphinx = False # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for MyST parser ------------------------------------------------- # Disable MyST syntax incompatible with vanilla mdformat # (i.e. disable everything except "directives") myst_disable_syntax = [ "table", "front_matter", "myst_line_comment", "myst_block_break", "myst_target", "myst_role", "math_inline", "math_block", "footnote_def", "footnote_inline", "footnote_ref", "footnote_tail", ] mdformat-0.7.22/docs/contributors/000077500000000000000000000000001475015777400171215ustar00rootroot00000000000000mdformat-0.7.22/docs/contributors/contributing.md000066400000000000000000000112761475015777400221610ustar00rootroot00000000000000# Contributing Welcome to the mdformat developer docs! We're excited you're here and want to contribute. ✨ Please discuss new features in an issue before submitting a PR to make sure that the feature is wanted and will be merged. Note that mdformat is an opinionated tool that attempts to keep formatting style changing configuration to its minimum. New configuration will only be added for a very good reason and use case. Below are the basic development steps. 1. Fork and clone the repository. 1. Install pre-commit hooks ```bash pre-commit install ``` 1. After making changes and having written tests, make sure tests pass: ```bash tox ``` 1. Test the pre-commit hook against the README.md file ```bash pre-commit try-repo . mdformat --files README.md ``` 1. Commit, push, and make a PR. ## Developing code formatter plugins Mdformat code formatter plugins need to define a formatter function that is of type `Callable[[str, str], str]`. The input arguments are the code block's unformatted code and info string, in that order. The return value should be formatted code. This function needs to be exposed via entry point distribution metadata. The entry point's group must be "mdformat.codeformatter", name must be name of the coding language it formats (as it appears in Markdown code block info strings), e.g. "python", and value has to point to the formatter function within the plugin package, e.g. "my_package.some_module:format_python" If using `setup.py` for packaging, the entry point configuration would have to be similar to: ```python import setuptools setuptools.setup( # other arguments here... entry_points={ "mdformat.codeformatter": ["python = my_package.some_module:format_python"] } ) ``` If using a PEP 621 compliant build backend (e.g. Flit) for packaging, the entry point configuration in `pyproject.toml` would need to be like: ```toml # other config here... [project.entry-points."mdformat.codeformatter"] "python" = "my_package.some_module:format_python" ``` For a real-world example plugin, see [mdformat-black](https://github.com/hukkin/mdformat-black), which formats Python code blocks with Black. ## Developing parser extension plugins The building blocks of an mdformat parses extension are typically: - Extend mdformat's CommonMark parser to parse the syntax extension. Mdformat uses [markdown-it-py](https://github.com/executablebooks/markdown-it-py) to parse. Note that markdown-it-py offers a range of extensions to the base CommonMark parser (see the [documented list](https://markdown-it-py.readthedocs.io/en/latest/plugins.html)), so there's a chance the extension already exists. - Activate the parser extension in mdformat. - Add rendering support for the new syntax. - Backslash escape the new syntax where applicable (typically either `text`, `inline` or `paragraph` renderers), to ensure mdformat doesn't render it when it must not. This could happen, for instance, when the syntax was backslash escaped in source Markdown. The easiest way to get started on a plugin, is to use the template repository. Mdformat parser extension plugins need to adhere to the `mdformat.plugins.ParserExtensionInterface`: ```python from collections.abc import Mapping from markdown_it import MarkdownIt from mdformat.renderer.typing import Render def update_mdit(mdit: MarkdownIt) -> None: """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" # A mapping from `RenderTreeNode.type` value to a `Render` function that can # render the given `RenderTreeNode` type. These functions override the default # `Render` funcs defined in `mdformat.renderer.DEFAULT_RENDERERS`. RENDERERS: Mapping[str, Render] ``` This interface needs to be exposed via entry point distribution metadata. The entry point's group must be "mdformat.parser_extension". If using `setup.py` for packaging, the entry point configuration would have to be similar to: ```python import setuptools setuptools.setup( # other arguments here... entry_points={ "mdformat.parser_extension": ["myextension = my_package:ext_module_or_class"] } ) ``` If using Poetry or Flit for packaging, the entry point configuration in `pyproject.toml` would need to be like: ```toml # Poetry specific: [tool.poetry.plugins."mdformat.parser_extension"] "myextension" = "my_package:ext_module_or_class" ``` ```toml # or PEP 621 compliant (works with Flit): [project.entry-points."mdformat.parser_extension"] "myextension" = "my_package:ext_module_or_class" ``` ## Making your plugin discoverable In case you host your plugin on GitHub, make sure to add it under the "mdformat" topic so it shows up on . mdformat-0.7.22/docs/index.md000066400000000000000000000024711475015777400160210ustar00rootroot00000000000000
# ![mdformat](_static/logo.svg) > CommonMark compliant Markdown formatter
```{include} ../README.md :start-after: :end-before: ``` The features/opinions of the formatter include: - Consistent indentation and whitespace across the board - Always use ATX style headings - Move all link references to the bottom of the document (sorted by label) - Reformat indented code blocks as fenced code blocks - Use `1.` as the ordered list marker if possible, also for noninitial list items Mdformat will not change word wrapping by default. The rationale for this is to support [Semantic Line Breaks](https://sembr.org/). For a comprehensive description and rationalization of the style, read [the style guide](users/style.md). ```{include} ../README.md :start-after: :end-before: ``` ```{toctree} --- caption: For users hidden: true --- users/installation_and_usage.md users/configuration_file.md users/plugins.md users/style.md users/changelog.md ``` ```{toctree} --- caption: For contributors hidden: true --- contributors/contributing.md ``` ```{toctree} --- caption: External links hidden: true --- GitHub repository PyPI page ``` mdformat-0.7.22/docs/requirements.txt000066400000000000000000000002141475015777400176450ustar00rootroot00000000000000# List dependencies in a format that readthedocs.org understands. docutils == 0.21.2 sphinx == 8.0.2 myst-parser == 4.0.0 furo == 2024.8.6 mdformat-0.7.22/docs/users/000077500000000000000000000000001475015777400155255ustar00rootroot00000000000000mdformat-0.7.22/docs/users/changelog.md000066400000000000000000000277351475015777400200140ustar00rootroot00000000000000# Changelog This log documents all Python API or CLI breaking backwards incompatible changes. Note that there is currently no guarantee for a stable Markdown formatting style across versions. ## 0.7.22 - Performance - Improved import time and performance of CLI invocations that do not format Markdown. - Fixed - No longer mutate cached TOML options or `_conf.DEFAULT_OPTS`. This should avoid some hard to debug issues in plugins' tests. - Added - `cache_toml` keyword argument to `_cli.run` for disabling TOML caching. This is useful in tests written for plugins. ## 0.7.21 - Fixed - Circular import in plugins that import from `mdformat.renderer`. ## 0.7.20 **NOTE:** This release was yanked from PyPI. - Deprecated - `mdformat.codepoints.ASCII_WHITESPACE`. CommonMark no longer defines this since v0.30. - Added - `--no-validate` flag for disabling the AST safety check. Thank you, [Kyle King](https://github.com/KyleKing), for the PR. - Added the delete control character to `mdformat.codepoints.ASCII_CTRL` as per CommonMark v0.30 - Fixed - The AST safety check not triggering when a code formatter plugin is in use, two or more code blocks are in the same file, and unsafe formatting happens in between the code blocks. ## 0.7.19 - Deprecated - Plugin interface: `mdformat.plugins.ParserExtensionInterface.add_cli_options`. The replacing interface is `mdformat.plugins.ParserExtensionInterface.add_cli_argument_group`. - Fixed - Incorrect line wrap on lines right after a hard break. Thank you, [MDW](https://github.com/mdeweerd), for the issue. - Adding an extra leading space to paragraphs that start with space in line wrap modes. - An error on empty paragraph (Unicode space only) surrounded by non-paragraph elements. Thank you, [Nico Schlömer](https://github.com/nschloe), for the issue. - Added - Plugin interface: `mdformat.plugins.ParserExtensionInterface.add_cli_argument_group`. With this plugins can now read CLI arguments merged with values from `.mdformat.toml`. - Option to select enabled (and required) extensions and code formatter languages (`--extensions` and `--codeformatters` on the CLI, and `extensions` and `codeformatters` keys in TOML). - Improved plugin list at the end of `--help` output: List languages supported by codeformatter plugin distributions, and parser extensions added by parser extension distributions. - Changed - Style: No longer escape square bracket enclosures. - Style: No longer escape less than sign followed by space character. - Style: Convert tabs to spaces. Reduce space sequences to one space. - Improved - Plugin interface: A trailing newline is added to fenced code blocks if a plugin fails to add it. ## 0.7.18 - Added - Option to exclude file paths using Unix-style glob patterns (`--exclude` on the CLI and `exclude` key in TOML). This feature is Python 3.13+ only. Thank you, [J. Sebastian Paez](https://github.com/jspaezp), for the issue. - Removed - Python 3.8 support ## 0.7.17 - Added - Do not update mtime if formatting result is identical to the file. Thank you, [Pierre Augier](https://github.com/paugier), for the issue and the PR. - Fixed - An error on empty paragraph (Unicode space only). Thank you, [Nico Schlömer](https://github.com/nschloe), for the issue. - File write fails if no permissions to write to the directory. Fixed by removing atomic writes. Thank you, [Guy Kisel](https://github.com/guykisel), for the issue. - File permissions change on rewrite. Thank you, [Keiichi Watanabe](https://github.com/keiichiw), for the issue. - Removed - Python 3.7 support ## 0.7.16 - Added - Option to keep line ending sequence from source file (`--end-of-line=keep`). Thank you, [Mark Tsuchida](https://github.com/marktsuchida), for the issue and [Jan Wille](https://github.com/Cube707) for the PR. - Fixed - `--check` not working with `--end-of-line=crlf`. Thank you, [Jan Wille](https://github.com/Cube707), for the issue. - Insignificant Unicode whitespace causing unstable formatting. Thank you, [Yamada_Ika](https://github.com/Yamada-Ika), for the issue. ## 0.7.15 - Fixed - `--wrap` converts Unicode whitespace to regular spaces and line feeds. Thank you, [Nico Schlömer](https://github.com/nschloe), for the issue. - Packaging - Use `setuptools` as build backend ## 0.7.14 - Added - Accept `os.PathLike[str]` as `mdformat.file` input. - Improved - Add filepath to warning message on code formatter plugin error. - Use `tomllib` in Python 3.11+. - Changed - Style: Sort numeric link references numerically. Thank you [Ryan Delaney](https://github.com/rpdelaney) for the PR. ## 0.7.13 - Fixed - Don't indent inline HTML that looks like type 7 block HTML. Thank you [Philip May](https://github.com/PhilipMay) for the issue. ## 0.7.12 - Fixed - Fix unstable formatting when a paragraph line starts with inline HTML. Thank you [Gabriel Nützi](https://github.com/gabyx) for the issue. ## 0.7.11 - Added - Support for `markdown-it-py` v2 - Fixed - Fix an error when a code fence info string starts with a tilde or a backtick. Thank you [Jonathan Newnham](https://github.com/jnnnnn) for the issue. ## 0.7.10 - Added - Support for configuration in a `.mdformat.toml` file - Removed - Python 3.6 support ## 0.7.9 - Fixed - Fix an error when an autolink contains URL encoded spaces. Thank you [Chris Butler](https://github.com/butler54) for the issue. ## 0.7.8 - Fixed - Fix a case where indented Markdown nested inside indented raw HTML tags would alter AST. Thank you [Jirka Borovec](https://github.com/Borda) for the issue. ## 0.7.7 - Fixed - Output `lf` line endings on all platforms. Thank you [Scott Gudeman](https://github.com/DragonCrafted87) for the issue and the PR. - Added - Configuration option for outputting `crlf` line endings: `--end-of-line=crlf` - Resolve symlinks and modify the link destination file only. ## 0.7.6 - Changed - Style: Reduce wrap width by indent size in lists and quotes ## 0.7.5 - Fixed - Error rendering a hard break in a heading - Some obscure leading/trailing whitespace issues - Style: Convert image description newlines to spaces in wrap altering modes ## 0.7.4 - Added - `mdformat.renderer.WRAP_POINT` for plugins to show where word wrap is allowed to occur. - `mdformat.renderer.RenderContext.do_wrap` for plugins to check whether word wrap is enabled. - Changed - Style: Emphasis and strong emphasis are now wrapped. - Style: Word wrap width target is now respected more precisely in a few edge cases. ## 0.7.3 - Fixed - Style: Convert link text newlines to spaces in wrap altering modes. - Changed - Style: No longer escape line starting hashes not followed by a space. ## 0.7.2 - Fixed - Style: Stop adding a newline character to empty documents. ## 0.7.1 - Added - `RenderContext.with_default_renderer_for`: A convenience method for copying a render context with a set of renderers set to defaults ## 0.7.0 **NOTE:** Parser extension plugin API has changed in this release. - Added - `POSTPROCESSORS` mapping to parser extension plugin API (i.e. `ParserExtensionInterface`), providing a way for plugins to render syntax collaboratively. - `Postprocess` type alias to `mdformat.renderer.typing` - `mdformat.renderer.RenderContext`: a context object passed as input to `Render` and `Postprocess` functions - Changed - Renamed `ParserExtensionInterface.RENDERER_FUNCS` as `ParserExtensionInterface.RENDERERS` - Renamed `mdformat.renderer.typing.RendererFunc` as `mdformat.renderer.typing.Render` - `mdformat.renderer.typing.Render` signature changed. Now takes `RenderContext` as input. - Renamed `mdformat.renderer.DEFAULT_RENDERER_FUNCS` as `mdformat.renderer.DEFAULT_RENDERERS` ## 0.6.4 - Fixed - Warnings being printed twice when wrap mode is other than "keep" ([#167](https://github.com/hukkin/mdformat/pull/167)) - An extra newline being added when consecutive lines' width equals wrap width ([#166](https://github.com/hukkin/mdformat/pull/166)) ## 0.6.3 - Added - A list of installed plugins and their versions in the output of `--help` and `--version` CLI commands - `mdformat.codepoints` as public API ## 0.6.2 - Added - Sphinx docs - Atomic file writes. Markdown content now stays on disk every nanosecond of the formatting process. ## 0.6.1 - Fixed - A line starting blockquote marker (">") is now escaped. ## 0.6.0 **NOTE:** Parser extension plugin API has changed in this release. - Removed - `mdformat.renderer.MARKERS` - `mdformat.plugins.ParserExtensionInterface.render_token` - `start` and `stop` keyword arguments removed from `mdformat.renderer.MDRenderer.render`. Use `mdformat.renderer.MDRenderer.render_tree` to render a part of a Markdown document. - Added - Modes for setting a word wrap width and removing word wrap - `mdformat.plugins.ParserExtensionInterface.RENDERER_FUNCS` - A class for representing linear `markdown-it` token stream as a tree: `mdformat.renderer.RenderTreeNode` - `mdformat.renderer.MDRenderer.render_tree` for rendering a `RenderTreeNode` ## 0.5.7 - Fixed - CLI crash when formatting standard error output and the operating system reports a terminal window width of zero or less ([#131](https://github.com/hukkin/mdformat/issues/131)). Thank you [ehontoria](https://github.com/ehontoria) for the issue. ## 0.5.6 - Changed - Style: Reduce asterisk escaping ([#120](https://github.com/hukkin/mdformat/issues/120)) - Style: Reduce underscore escaping ([#119](https://github.com/hukkin/mdformat/issues/119)). Thank you [dustinmichels](https://github.com/dustinmichels) for the issue. ## 0.5.5 - Changed - Style: Don't convert shortcut reference links into full reference links ([#111](https://github.com/hukkin/mdformat/issues/111)) ## 0.5.4 - Changed - Style: Reduce hash (`#`) escaping ## 0.5.0 - Changed - Style: Convert list marker types. Prefer "-" for bullet lists and "." for ordered lists. - Style: Remove trailing whitespace from empty list items. ## 0.4.0 - Changed - Style: Only surround link destination with angle brackets if required by CommonMark spec - Style: Thematic breaks are now 70 characters wide ## 0.3.5 - Fixed - Markdown equality validation falsely triggering when code formatter plugins were used. Thanks [chrisjsewell](https://github.com/chrisjsewell) for writing the tests to find the bug. ## 0.3.3 - Added - `CHANGES_AST` to extension plugin API. The feature allows plugins that alter Markdown AST to skip validation ([#49](https://github.com/hukkin/mdformat/pull/49)). ## 0.3.2 - Changed - Style: Keep reference links as reference links ([#32](https://github.com/hukkin/mdformat/issues/32)). Thank you [chrisjsewell](https://github.com/chrisjsewell) for the issue and the PR. - Added - Option to number ordered list items consecutively using the `--number` flag ([#33](https://github.com/hukkin/mdformat/issues/33)). Thank you [chrisjsewell](https://github.com/chrisjsewell) for the issue and the PR. - Parser extension plugins can now add their own CLI / Python API options ([#35](https://github.com/hukkin/mdformat/pull/35)). Thanks [chrisjsewell](https://github.com/chrisjsewell) for the PR. - Fixed - Image links that require surrounding angle brackets no longer break formatting ([#40](https://github.com/hukkin/mdformat/issues/40)). ## 0.3.1 - Added - Plugin system for extending the parser ([#13](https://github.com/hukkin/mdformat/issues/13)). Thank you [chrisjsewell](https://github.com/chrisjsewell) for the issue and the PR. - Exported `mdformat.renderer.MDRenderer` and `mdformat.renderer.MARKERS` ## 0.3.0 - Changed - Code formatter plugin function signature changed to `Callable[[str, str], str]`. The second input argument is full info string of the code block. ## 0.2.0 - Changed - Style: Use backtick for code fences whenever possible ## 0.1.2 - Added - Support for code formatter plugins ## 0.1.0 - Added - Initial mdformat release mdformat-0.7.22/docs/users/configuration_file.md000066400000000000000000000044651475015777400217260ustar00rootroot00000000000000# Configuration file Mdformat allows configuration in a [TOML](https://toml.io) file named `.mdformat.toml`. The configuration file will be resolved starting from the location of the file being formatted, and searching up the file tree until a config file is (or isn't) found. When formatting standard input stream, resolution will be started from current working directory. Command line interface arguments take precedence over the configuration file. ## Example configuration ```toml # .mdformat.toml # # This file shows the default values and is equivalent to having # no configuration file at all. Change the values for non-default # behavior. # wrap = "keep" # options: {"keep", "no", INTEGER} number = false # options: {false, true} end_of_line = "lf" # options: {"lf", "crlf", "keep"} validate = true # options: {false, true} # extensions = [ # options: a list of enabled extensions (default: all installed are enabled) # "gfm", # "toc", # ] # codeformatters = [ # options: a list of enabled code formatter languages (default: all installed are enabled) # "python", # "json", # ] # Python 3.13+ only: exclude = [] # options: a list of file path pattern strings ``` ## Exclude patterns A list of file exclusion patterns can be defined on Python 3.13+. Unix-style glob patterns are supported, see [Python's documentation](https://docs.python.org/3/library/pathlib.html#pattern-language) for syntax definition. Glob patterns are matched against relative paths. If `--exclude` is used on the command line, the paths are relative to current working directory. Else the paths are relative to the parent directory of the file's `.mdformat.toml`. Only files (recursively) contained by the base directory can be excluded. Files that match an exclusion pattern are _always_ excluded, even in the case that they are directly referenced in a command line invocation. ### Example patterns ```toml # .mdformat.toml exclude = [ "CHANGELOG.md", # exclude a single root level file "venv/**", # recursively exclude a root level directory "**/node_modules/**", # recursively exclude a directory at any level "**/*.txt", # exclude all .txt files "**/*.m[!d]", "**/*.[!m]d", # exclude all files that are not suffixed .md ] ``` mdformat-0.7.22/docs/users/installation_and_usage.md000066400000000000000000000031161475015777400225570ustar00rootroot00000000000000# Installation and usage ```{include} ../../README.md :start-after: :end-before: ``` ```{warning} The formatting style produced by mdformat may change in each version. It is recommended to pin mdformat dependency version. ``` ```{include} ../../README.md :start-after: :end-before: ``` ## Python API usage ### Format text ```python import mdformat unformatted = "\n\n# A header\n\n" formatted = mdformat.text(unformatted) assert formatted == "# A header\n" ``` ### Format a file Format file `README.md` in place: ```python import mdformat # Input filepath as a string... mdformat.file("README.md") # ...or a pathlib.Path object import pathlib filepath = pathlib.Path("README.md") mdformat.file(filepath) ``` ### Options All formatting style modifying options available in the CLI are also available in the Python API, with equivalent option names: ```python import mdformat mdformat.file( "FILENAME.md", options={ "number": True, # switch on consecutive numbering of ordered lists "wrap": 60, # set word wrap width to 60 characters } ) ``` ## Usage as a pre-commit hook `mdformat` can be used as a [pre-commit](https://github.com/pre-commit/pre-commit) hook. Add the following to your project's `.pre-commit-config.yaml` to enable this: ```yaml - repo: https://github.com/hukkin/mdformat rev: 0.7.22 # Use the ref you want to point at hooks: - id: mdformat # Optionally add plugins additional_dependencies: - mdformat-gfm - mdformat-black ``` mdformat-0.7.22/docs/users/plugins.md000066400000000000000000000162661475015777400175430ustar00rootroot00000000000000# Plugins Mdformat offers an extensible plugin system for code fence content formatting, Markdown parser extensions (like GFM tables), and modifying/adding other functionality. This document explains how to use plugins. If you want to create a new plugin, refer to the [contributing](../contributors/contributing.md) docs. ## Code formatter plugins Mdformat features a plugin system to support formatting of Markdown code blocks where the coding language has been labeled. For instance, if [`mdformat-black`](https://github.com/hukkin/mdformat-black) plugin is installed in the environment, mdformat CLI will automatically format Python code blocks with [Black](https://github.com/psf/black). For stability, mdformat Python API behavior will not change simply due to a plugin being installed. Code formatters will have to be explicitly enabled in addition to being installed: ````python import mdformat unformatted = "```python\n'''black converts quotes'''\n```\n" # Pass in `codeformatters` here! It is an iterable of coding languages # that should be formatted formatted = mdformat.text(unformatted, codeformatters={"python"}) assert formatted == '```python\n"""black converts quotes"""\n```\n' ```` ### Existing plugins This is a curated list of popular code formatter plugins. The list is not exhaustive. Explore mdformat's [GitHub topic](https://github.com/topics/mdformat) for more.
Distribution Supported languages Notes
mdformat-beautysh bash, sh
mdformat-black python
mdformat-config json, toml, yaml
mdformat-gofmt go Requires Go installation
mdformat-ruff python
mdformat-rustfmt rust Requires rustfmt installation
mdformat-shfmt bash, sh Requires either shfmt, Docker or Podman installation
mdformat-web javascript, js, css, html, xml
## Parser extension plugins By default, mdformat only parses and renders [CommonMark](https://spec.commonmark.org/current/). Installed plugins can add extensions to the syntax, such as footnotes, tables, and other document elements. For stability, mdformat Python API behavior will not change simply due to a plugin being installed. Extensions will have to be explicitly enabled in addition to being installed: ```python import mdformat unformatted = "content...\n" # Pass in `extensions` here! It is an iterable of extensions that should be loaded formatted = mdformat.text(unformatted, extensions={"tables"}) ``` ### Existing plugins This is a curated list of popular parser extension plugins. The list is not exhaustive. Explore mdformat's [GitHub topic](https://github.com/topics/mdformat) for more.
Distribution Plugins Description
mdformat-admon admonition Adds support for python-markdown admonitions
mdformat-deflist deflist Adds support for Pandoc-style definition lists
mdformat-footnote footnote Adds support for Pandoc-style footnotes
mdformat-frontmatter frontmatter Adds support for front matter, and formats YAML front matter
mdformat-gfm gfm Changes target specification to GitHub Flavored Markdown (GFM)
mdformat-gfm-alerts gfm_alerts Extends GitHub Flavored Markdown (GFM) with "Alerts"
mdformat-mkdocs mkdocs Changes target specification to MKDocs. Indents lists with 4-spaces instead of 2
mdformat-myst myst Changes target specification to MyST
mdformat-tables tables Adds support for GitHub Flavored Markdown style tables
mdformat-toc toc Adds the capability to auto-generate a table of contents
## Other misc plugins ### Existing plugins This is a curated list of other plugins that don't fit the above categories. The list is not exhaustive. Explore mdformat's [GitHub topic](https://github.com/topics/mdformat) for more.
Distribution Plugins Description
mdformat-pyproject pyproject Adds support for loading options from a [tool.mdformat] section inside the pyproject.toml file, if it exists
mdformat-simple-breaks simple_breaks Render thematic breaks using three dashes instead of 70 underscores
mdformat-0.7.22/docs/users/style.md000066400000000000000000000130421475015777400172070ustar00rootroot00000000000000# Formatting style This document describes, demonstrates, and rationalizes the formatting style that mdformat follows. Mdformat's formatting style is crafted so that writing, editing and collaborating on Markdown documents is as smooth as possible. The style is consistent, and minimizes diffs (for ease of reviewing changes), sometimes at the cost of some readability. Mdformat makes sure to only change style, not content. Once converted to HTML and rendered on screen, formatted Markdown should yield a result that is visually identical to the unformatted document. Mdformat CLI includes a safety check that will error and refuse to apply changes to a file if Markdown AST is not equal before and after formatting. ## Headings For consistency, only ATX headings are used. Setext headings are reformatted using the ATX style. ATX headings are used because they can be consistently used for any heading level, whereas setext headings only allow level 1 and 2 headings. Input: ```markdown First level heading === Second level heading --- ``` Output: ```markdown # First level heading ## Second level heading ``` ## Bullet lists Mdformat uses `-` as the bullet list marker. In the case of consecutive bullet lists, mdformat alternates between `-` and `*` markers. ## Ordered lists Mdformat uses `.` as ordered list marker type. In the case of consecutive ordered lists, mdformat alternates between `.` and `)` types. Mdformat uses `1.` or `1)` as the ordered list marker, also for noninital list items. Input: ```markdown 1. Item A 2. Item B 3. Item C ``` Output: ```markdown 1. Item A 1. Item B 1. Item C ``` This "non-numbering" style was chosen to minimize diffs. But how exactly? Lets imagine we are listing the alphabets, using a proper consecutive numbering style: ```markdown 1. b 2. c 3. d ``` Now we notice an error was made, and that the first character "a" is missing. We add it as the first item in the list. As a result, the numbering of every subsequent item in the list will increase by one, meaning that the diff will touch every line in the list. The non-numbering style solves this issue: only the added line will show up in the diff. Mdformat allows consecutive numbering via configuration. ## Code blocks Only fenced code blocks are allowed. Indented code blocks are reformatted as fenced code blocks. Fenced code blocks are preferred because they allow setting an info string, which indented code blocks do not support. ## Code spans Length of a code span starting/ending backtick string is reduced to minimum. Needless space characters are stripped from the front and back, unless the content contains backticks. Input: `````markdown ````Backtick string is reduced.```` ` Space is stripped from the front and back... ` ```` ...unless a "`" character is present. ```` ````` Output: ```markdown `Backtick string is reduced.` `Space is stripped from the front and back...` `` ...unless a "`" character is present. `` ``` ## Inline links Redundant angle brackets surrounding a link destination will be removed. Input: ```markdown [Python]() ``` Output: ```markdown [Python](https://python.org) ``` ## Reference links All link reference definitions are moved to the bottom of the document, sorted by label. Unused and duplicate references are removed. Input: ```markdown [dupe ref]: https://gitlab.com [dupe ref]: link1 [unused ref]: link2 Here's a link to [GitLab][dupe ref] ``` Output: ```markdown Here's a link to [GitLab][dupe ref] [dupe ref]: https://gitlab.com ``` ## Paragraph word wrapping Mdformat by default will not change word wrapping. The rationale for this is to encourage and support [Semantic Line Breaks](https://sembr.org/), a technique described by Brian Kernighan in the early 1970s, yet still as relevant as ever today: > **Hints for Preparing Documents** > > Most documents go through several versions (always more than you > expected) before they are finally finished. Accordingly, you should > do whatever possible to make the job of changing them easy. > > First, when you do the purely mechanical operations of typing, type > so subsequent editing will be easy. Start each sentence on a new line. > Make lines short, and break lines at natural places, such as after > commas and semicolons, rather than > randomly. Since > most people change documents by rewriting phrases and adding, deleting > and rearranging sentences, these precautions simplify any editing you > have to do later. > > _— Brian W. Kernighan. "UNIX for Beginners". 1974_ Mdformat allows removing word wrap or setting a target wrap width via configuration. ## Thematic breaks Thematic breaks are formatted as a 70 character wide string of underscores. A wide thematic break is distinguishable, and visually resembles how a corresponding HTML `
` tag is typically rendered. ## Whitespace Mdformat applies consistent whitespace across the board: - Convert line endings to a single newline character - Strip paragraph trailing and leading whitespace - Indent contents of block quotes and list items consistently - Always separate blocks with a single empty line (an exception being tight lists where the separator is a single newline character) - Always end the document in a single newline character (an exception being an empty document) ## Hard line breaks Hard line breaks are always a backslash preceding a line ending. The alternative syntax, two or more spaces before a line ending, is not used because it is not visible. Input: ```markdown Hard line break is here: Can you see it? ``` Output: ```markdown Hard line break is here:\ Can you see it? ``` mdformat-0.7.22/fuzzer/000077500000000000000000000000001475015777400147615ustar00rootroot00000000000000mdformat-0.7.22/fuzzer/fuzz.py000066400000000000000000000027621475015777400163400ustar00rootroot00000000000000import atheris with atheris.instrument_imports(): import hashlib import sys import warnings import mdformat from mdformat._util import is_md_equal # Suppress all warnings. warnings.simplefilter("ignore") def test_one_input(input_bytes: bytes) -> None: # We need a Unicode string, not bytes fdp = atheris.FuzzedDataProvider(input_bytes) data = fdp.ConsumeUnicode(sys.maxsize) try: formatted_data = mdformat.text(data) except BaseException: handle_err(data) raise if not is_md_equal(data, formatted_data): handle_err(data) raise Exception("Formatted Markdown not equal!") def handle_err(data): codepoints = [hex(ord(x)) for x in data] sys.stderr.write(f"Input was {type(data)}:\n{data}\nCodepoints:\n{codepoints}\n") # Atheris already writes crash data to a file, but it seems it is not UTF-8 encoded. # I'm not sure what the encoding is exactly. Anyway, let's write another file here # that is guaranteed to be valid UTF-8. data_bytes = data.encode() filename = "crash-utf8-" + hashlib.sha256(data_bytes).hexdigest() with open(filename, "wb") as f: f.write(data_bytes) sys.stderr.write(f"Wrote UTF-8 encoded data to {filename}\n") sys.stderr.flush() def main(): # For possible options, see https://llvm.org/docs/LibFuzzer.html#options fuzzer_options = sys.argv atheris.Setup(fuzzer_options, test_one_input) atheris.Fuzz() if __name__ == "__main__": main() mdformat-0.7.22/fuzzer/requirements.txt000066400000000000000000000000631475015777400202440ustar00rootroot00000000000000# sudo apt-get install clang wheel atheris==2.0.12 mdformat-0.7.22/pyproject.toml000066400000000000000000000131071475015777400163520ustar00rootroot00000000000000[build-system] requires = ["setuptools>=69"] build-backend = "setuptools.build_meta" [project] name = "mdformat" version = "0.7.22" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT description = "CommonMark compliant Markdown formatter" authors = [ { name = "Taneli Hukkinen", email = "hukkin@users.noreply.github.com" }, ] license = { file = "LICENSE" } requires-python = ">=3.9" dependencies = [ 'markdown-it-py >=1.0.0,<4.0.0', 'tomli >=1.1.0; python_version < "3.11"', 'importlib-metadata >=3.6.0; python_version < "3.10"', ] readme = "README.md" classifiers = [ "Environment :: Console", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Topic :: Documentation", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Markup", "Typing :: Typed", ] keywords = ["mdformat", "markdown", "commonmark", "formatter", "pre-commit"] [project.urls] "Homepage" = "https://github.com/hukkin/mdformat" "Documentation" = "https://mdformat.readthedocs.io" "Style guide" = "https://mdformat.readthedocs.io/en/stable/users/style.html" "Changelog" = "https://mdformat.readthedocs.io/en/stable/users/changelog.html" [project.scripts] mdformat = "mdformat.__main__:run" [tool.isort] # Force imports to be sorted by module, independent of import type force_sort_within_sections = true # Group first party and local folder imports together no_lines_before = ["LOCALFOLDER"] # Configure isort to work without access to site-packages known_first_party = ["mdformat", "tests"] # Settings for Black compatibility profile = "black" [tool.pytest.ini_options] addopts = "--strict-markers --strict-config" xfail_strict = true [tool.tox] requires = ["tox>=4.21.1"] # Only run pytest envs when no args given to tox env_list = ["3.9", "3.10", "3.11", "3.12", "3.13"] [tool.tox.env_run_base] description = "run tests under {base_python}" deps = [ "-r tests/requirements.txt", ] commands = [ ["pytest", { replace = "posargs", default = ["--cov"], extend = true }], ] [tool.tox.env."profile"] description = "run profiler (use e.g. `firefox .tox/prof/combined.svg` to open)" deps = [ "-r tests/requirements.txt", "pytest-profiling", ] commands = [ ["pytest", "tests/test_for_profiler.py", "--profile-svg", "--pstats-dir", "{toxworkdir}/prof"], ["python", "-c", 'import pathlib; print("profiler svg output under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "prof" / "combined.svg"))'], ] [tool.tox.env."pre-commit"] description = "run linters (except mypy)" skip_install = true deps = ["pre-commit"] commands = [ ["pre-commit", "run", { replace = "posargs", default = ["--all-files"], extend = true }], ] [tool.tox.env."mypy"] description = "run mypy" basepython = ["python3.11"] deps = [ "-r tests/requirements.txt", "mypy==1.13.0", ] commands = [ ["mypy", { replace = "posargs", default = ["src/", "tests/"], extend = true }], ] [tool.tox.env."hook"] description = "test mdformat's own pre-commit hook against the README file" skip_install = true deps = ["pre-commit"] commands = [ ["pre-commit", "try-repo", ".", "mdformat", "--files", "README.md"], ] [tool.tox.env."cli"] description = "run mdformat's own CLI" deps = [] commands = [ ["mdformat", { replace = "posargs", extend = true }], ] [tool.tox.env."docs"] description = "invoke sphinx-build to build the HTML docs" skip_install = true deps = [ "-r docs/requirements.txt", ] commands = [ ["sphinx-build", "-d", "{toxworkdir}/docs_doctree", "docs/", "{toxworkdir}/docs_out", "-W", "-b", "html", { replace = "posargs", extend = true }], ["python", "-c", 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))'], ] [tool.tox.env."fuzz"] description = 'run the fuzzer (needs "apt install clang")' deps = [ "-r fuzzer/requirements.txt", ] allowlist_externals = [ "mkdir", "cp", ] commands = [ # Create a folder for persistent corpus and use README.md as initial seed ["mkdir", "-p", "{toxworkdir}/fuzzer-corpus"], ["cp", "-n", "README.md", "{toxworkdir}/fuzzer-corpus/README.md"], # Run fuzzer ["python", "fuzzer/fuzz.py", "{toxworkdir}/fuzzer-corpus", { replace = "posargs", default = ["-len_control=10000"], extend = true }], ] [tool.tox.env."benchmark"] description = "benchmark mdformat against local doc files" deps = [] commands = [ ["python", "-c", "print('Wrap mode: keep')"], ["python", "-m", "timeit", "from mdformat._cli import run", 'run(["README.md", "docs/", "--check"])'], ["python", "-c", "print('Wrap mode: 50')"], ["python", "-m", "timeit", "from mdformat._cli import run", 'run(["README.md", "docs/", "--check", "--wrap", "50"])'], ] [tool.tox.env."benchmark-import"] description = "Measure module import times. Tox sends mdformat output to stderr, so to filter use e.g. `tox -e benchmark-import 2> >(grep mdformat)`." deps = [] commands = [ ["python", "-X", "importtime", "-m", "mdformat", "--version"], ] [tool.coverage.run] source = ["mdformat"] plugins = ["covdefaults"] [tool.mypy] show_error_codes = true warn_unreachable = true warn_unused_ignores = true warn_redundant_casts = true warn_unused_configs = true # Disabling incremental mode is required for `warn_unused_configs = true` to work incremental = false disallow_untyped_defs = true check_untyped_defs = true strict_equality = true implicit_reexport = false no_implicit_optional = true [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false mdformat-0.7.22/src/000077500000000000000000000000001475015777400142235ustar00rootroot00000000000000mdformat-0.7.22/src/mdformat/000077500000000000000000000000001475015777400160345ustar00rootroot00000000000000mdformat-0.7.22/src/mdformat/__init__.py000066400000000000000000000002321475015777400201420ustar00rootroot00000000000000__all__ = ("file", "text") __version__ = "0.7.22" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT from mdformat._api import file, text mdformat-0.7.22/src/mdformat/__main__.py000066400000000000000000000003051475015777400201240ustar00rootroot00000000000000import sys from typing import NoReturn import mdformat._cli def run() -> NoReturn: exit_code = mdformat._cli.run(sys.argv[1:]) sys.exit(exit_code) if __name__ == "__main__": run() mdformat-0.7.22/src/mdformat/_api.py000066400000000000000000000045471475015777400173300ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable, Mapping from contextlib import AbstractContextManager from os import PathLike from pathlib import Path from typing import Any from mdformat._conf import DEFAULT_OPTS from mdformat._util import EMPTY_MAP, NULL_CTX, build_mdit, detect_newline_type def text( md: str, *, options: Mapping[str, Any] = EMPTY_MAP, extensions: Iterable[str] = (), codeformatters: Iterable[str] = (), _first_pass_contextmanager: AbstractContextManager = NULL_CTX, _filename: str = "", ) -> str: """Format a Markdown string.""" # Lazy import to improve module import time from mdformat.renderer import MDRenderer with _first_pass_contextmanager: mdit = build_mdit( MDRenderer, mdformat_opts={**options, **{"filename": _filename}}, extensions=extensions, codeformatters=codeformatters, ) rendering = mdit.render(md) # If word wrap is changed, add a second pass of rendering. # Some escapes will be different depending on word wrap, so # rendering after 1st and 2nd pass will be different. Rendering # twice seems like the easiest way to achieve stable formatting. if options.get("wrap", DEFAULT_OPTS["wrap"]) != "keep": rendering = mdit.render(rendering) return rendering def file( f: str | PathLike[str], *, options: Mapping[str, Any] = EMPTY_MAP, extensions: Iterable[str] = (), codeformatters: Iterable[str] = (), ) -> None: """Format a Markdown file in place.""" f = Path(f) try: is_file = f.is_file() except OSError: # Catch "OSError: [WinError 123]" on Windows # pragma: no cover is_file = False if not is_file: raise ValueError(f'Cannot format "{f}". It is not a file.') if f.is_symlink(): raise ValueError(f'Cannot format "{f}". It is a symlink.') original_md = f.read_bytes().decode() formatted_md = text( original_md, options=options, extensions=extensions, codeformatters=codeformatters, _filename=str(f), ) newline = detect_newline_type( original_md, options.get("end_of_line", DEFAULT_OPTS["end_of_line"]) ) formatted_md = formatted_md.replace("\n", newline) if formatted_md != original_md: f.write_bytes(formatted_md.encode()) mdformat-0.7.22/src/mdformat/_cli.py000066400000000000000000000415051475015777400173210ustar00rootroot00000000000000from __future__ import annotations import argparse from collections.abc import Generator, Iterable, Mapping, Sequence import contextlib import functools import logging import os.path from pathlib import Path import shutil import sys import textwrap import mdformat from mdformat._conf import DEFAULT_OPTS, InvalidConfError, read_toml_opts from mdformat._util import detect_newline_type, is_md_equal import mdformat.plugins class RendererWarningPrinter(logging.Handler): def emit(self, record: logging.LogRecord) -> None: if record.levelno >= logging.WARNING: # pragma: no branch sys.stderr.write(f"Warning: {record.msg}\n") def run(cli_args: Sequence[str], cache_toml: bool = True) -> int: # noqa: C901 arg_parser = make_arg_parser( mdformat.plugins._PARSER_EXTENSION_DISTS, mdformat.plugins._CODEFORMATTER_DISTS, mdformat.plugins.PARSER_EXTENSIONS, ) cli_opts = { k: v for k, v in vars(arg_parser.parse_args(cli_args)).items() if v is not None } cli_core_opts, cli_plugin_opts = separate_core_and_plugin_opts(cli_opts) if not cli_opts["paths"]: print_paragraphs(["No files have been passed in. Doing nothing."]) return 0 try: file_paths = resolve_file_paths(cli_opts["paths"]) except InvalidPath as e: arg_parser.error(f'File "{e.path}" does not exist.') format_errors_found = False renderer_warning_printer = RendererWarningPrinter() for path in file_paths: read_toml = read_toml_opts if cache_toml else read_toml_opts.__wrapped__ try: toml_opts, toml_path = read_toml(path.parent if path else Path.cwd()) except InvalidConfError as e: print_error(str(e)) return 1 opts = {**DEFAULT_OPTS, **toml_opts, **cli_core_opts} # Merge plugin options from CLI. # Make a copy of opts["plugin"] to not mutate DEFAULT_OPTS or cached TOML. opts["plugin"] = dict(opts["plugin"]) for plugin_id, plugin_opts in cli_plugin_opts.items(): if plugin_id in opts["plugin"]: opts["plugin"][plugin_id] |= plugin_opts else: opts["plugin"][plugin_id] = plugin_opts if sys.version_info >= (3, 13): # pragma: >=3.13 cover if is_excluded(path, opts["exclude"], toml_path, "exclude" in cli_opts): continue else: # pragma: <3.13 cover if "exclude" in toml_opts: print_error( "'exclude' patterns are only available on Python 3.13+.", paragraphs=[ "Please remove the 'exclude' list from your .mdformat.toml" " or upgrade Python version." ], ) return 1 try: enabled_parserplugins = ( mdformat.plugins.PARSER_EXTENSIONS if opts["extensions"] is None else { k: mdformat.plugins.PARSER_EXTENSIONS[k] for k in opts["extensions"] } ) except KeyError as e: print_error( "Invalid extension required.", paragraphs=[ f"The required {e.args[0]!r} extension is not available. " "Please install a plugin that adds the extension, " "or remove it from required extensions." ], ) return 1 try: enabled_codeformatters = ( mdformat.plugins.CODEFORMATTERS if opts["codeformatters"] is None else { k: mdformat.plugins.CODEFORMATTERS[k] for k in opts["codeformatters"] } ) except KeyError as e: print_error( "Invalid code formatter required.", paragraphs=[ f"The required {e.args[0]!r} code formatter language " "is not available. " "Please install a plugin " "that adds support for the language, " "or remove it from required languages." ], ) return 1 if path: path_str = str(path) # Unlike `path.read_text(encoding="utf-8")`, this preserves # line ending type. original_str = path.read_bytes().decode() else: path_str = "-" original_str = sys.stdin.read() # Lazy import to improve module import time from mdformat.renderer import LOGGER as RENDERER_LOGGER formatted_str = mdformat.text( original_str, options=opts, extensions=enabled_parserplugins, codeformatters=enabled_codeformatters, _first_pass_contextmanager=log_handler_applied( RENDERER_LOGGER, renderer_warning_printer ), _filename=path_str, ) newline = detect_newline_type(original_str, opts["end_of_line"]) formatted_str = formatted_str.replace("\n", newline) if opts["check"]: if formatted_str != original_str: format_errors_found = True print_error(f'File "{path_str}" is not formatted.') else: changes_ast = any( getattr(plugin, "CHANGES_AST", False) for plugin in enabled_parserplugins.values() ) if ( opts["validate"] and not changes_ast and not is_md_equal( original_str, formatted_str, options=opts, extensions=enabled_parserplugins, codeformatters=enabled_codeformatters, ) ): print_error( f'Could not format "{path_str}".', paragraphs=[ "Formatted Markdown renders to different HTML than input Markdown. " # noqa: E501 "This is a bug in mdformat or one of its installed plugins. " "Please retry without any plugins installed. " "If this error persists, " "report an issue including the input Markdown " "on https://github.com/hukkin/mdformat/issues. " "If not, " "report an issue on the malfunctioning plugin's issue tracker.", ], ) return 1 if path: if formatted_str != original_str: path.write_bytes(formatted_str.encode()) else: sys.stdout.buffer.write(formatted_str.encode()) if format_errors_found: return 1 return 0 def validate_wrap_arg(value: str) -> str | int: if value in {"keep", "no"}: return value width = int(value) if width < 1: raise ValueError("wrap width must be a positive integer") return width def make_arg_parser( parser_extension_dists: Mapping[str, tuple[str, list[str]]], codeformatter_dists: Mapping[str, tuple[str, list[str]]], parser_extensions: Mapping[str, mdformat.plugins.ParserExtensionInterface], ) -> argparse.ArgumentParser: epilog = get_plugin_info_str(parser_extension_dists, codeformatter_dists) parser = argparse.ArgumentParser( description="CommonMark compliant Markdown formatter", epilog=(epilog if epilog else None), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("paths", nargs="*", help="files to format") parser.add_argument( "--check", action="store_true", help="do not apply changes to files" ) parser.add_argument( "--no-validate", action="store_const", const=False, dest="validate", help="do not validate that the rendered HTML is consistent", ) version_str = f"mdformat {mdformat.__version__}" plugin_version_str = get_plugin_version_str( {**parser_extension_dists, **codeformatter_dists} ) if plugin_version_str: version_str += f" ({plugin_version_str})" parser.add_argument("--version", action="version", version=version_str) parser.add_argument( "--number", action="store_const", const=True, help="apply consecutive numbering to ordered lists", ) parser.add_argument( "--wrap", type=validate_wrap_arg, metavar="{keep,no,INTEGER}", help="paragraph word wrap mode (default: keep)", ) parser.add_argument( "--end-of-line", choices=("lf", "crlf", "keep"), help="output file line ending mode (default: lf)", ) if sys.version_info >= (3, 13): # pragma: >=3.13 cover parser.add_argument( "--exclude", action="append", metavar="PATTERN", help="exclude files that match the Unix-style glob pattern " "(multiple allowed)", ) extensions_group = parser.add_mutually_exclusive_group() extensions_group.add_argument( "--extensions", action="append", metavar="EXTENSION", help="require and enable an extension plugin " "(multiple allowed) " "(use `--no-extensions` to disable) " "(default: all enabled)", ) extensions_group.add_argument( "--no-extensions", action="store_const", const=(), dest="extensions", help=argparse.SUPPRESS, ) codeformatters_group = parser.add_mutually_exclusive_group() codeformatters_group.add_argument( "--codeformatters", action="append", metavar="LANGUAGE", help="require and enable a code formatter plugin " "(multiple allowed) " "(use `--no-codeformatters` to disable) " "(default: all enabled)", ) codeformatters_group.add_argument( "--no-codeformatters", action="store_const", const=(), dest="codeformatters", help=argparse.SUPPRESS, ) for plugin in parser_extensions.values(): if hasattr(plugin, "add_cli_options"): import warnings plugin_file, plugin_line = get_source_file_and_line(plugin) warnings.warn_explicit( "`mdformat.plugins.ParserExtensionInterface.add_cli_options`" " is deprecated." " Please use `add_cli_argument_group`.", DeprecationWarning, filename=plugin_file, lineno=plugin_line, ) plugin.add_cli_options(parser) for plugin_id, plugin in parser_extensions.items(): if hasattr(plugin, "add_cli_argument_group"): group = parser.add_argument_group(title=f"{plugin_id} plugin") plugin.add_cli_argument_group(group) for action in group._group_actions: action.dest = f"plugin.{plugin_id}.{action.dest}" return parser def separate_core_and_plugin_opts(opts: Mapping) -> tuple[dict, dict]: """Move dotted keys like 'plugin.gfm.some_key' to a separate mapping. Return a tuple of two mappings. First is for core CLI options, the second for plugin options. E.g. 'plugin.gfm.some_key' belongs to the second mapping under {"gfm": {"some_key": }}. """ cli_core_opts = {} cli_plugin_opts: dict = {} for k, v in opts.items(): if k.startswith("plugin."): _, plugin_id, plugin_conf_key = k.split(".", maxsplit=2) if plugin_id in cli_plugin_opts: cli_plugin_opts[plugin_id][plugin_conf_key] = v else: cli_plugin_opts[plugin_id] = {plugin_conf_key: v} else: cli_core_opts[k] = v return cli_core_opts, cli_plugin_opts class InvalidPath(Exception): """Exception raised when a path does not exist.""" def __init__(self, path: Path): self.path = path def resolve_file_paths(path_strings: Iterable[str]) -> list[None | Path]: """Resolve pathlib.Path objects from filepath strings. Convert path strings to pathlib.Path objects. Check that all paths are either files, directories or stdin. If not, raise InvalidPath. Resolve directory paths to a list of file paths (ending with ".md"). """ file_paths: list[None | Path] = [] # Path to file or None for stdin/stdout for path_str in path_strings: if path_str == "-": file_paths.append(None) continue path_obj = Path(path_str) path_obj = _normalize_path(path_obj) if path_obj.is_dir(): for p in path_obj.glob("**/*.md"): if p.is_file(): p = _normalize_path(p) file_paths.append(p) elif path_obj.is_file(): # pragma: nt no cover file_paths.append(path_obj) else: # pragma: nt no cover raise InvalidPath(path_obj) return file_paths def is_excluded( # pragma: >=3.13 cover path: Path | None, patterns: list[str], toml_path: Path | None, excludes_from_cli: bool, ) -> bool: if not path: return False if not excludes_from_cli and toml_path: exclude_root = toml_path.parent else: exclude_root = Path.cwd() try: relative_path = path.relative_to(exclude_root) except ValueError: return False return any( relative_path.full_match(pattern) # type: ignore[attr-defined] for pattern in patterns ) def _normalize_path(path: Path) -> Path: """Normalize path. Make the path absolute, resolve any ".." sequences. Do not resolve symlinks, as it would interfere with 'exclude' patterns. Raise `InvalidPath` if the path does not exist. """ path = Path(os.path.abspath(path)) try: path_exists = path.exists() except OSError: # Catch "OSError: [WinError 123]" on Windows # pragma: no cover path_exists = False if not path_exists: raise InvalidPath(path) return path def print_paragraphs(paragraphs: Iterable[str]) -> None: assert not isinstance(paragraphs, str) sys.stderr.write(wrap_paragraphs(paragraphs)) def print_error(title: str, paragraphs: Iterable[str] = ()) -> None: assert not isinstance(paragraphs, str) assert not title.lower().startswith("error") title = "Error: " + title paragraphs = [title] + list(paragraphs) print_paragraphs(paragraphs) @functools.lru_cache def cached_textwrapper(width: int) -> textwrap.TextWrapper: return textwrap.TextWrapper( break_long_words=False, break_on_hyphens=False, width=width, expand_tabs=False, replace_whitespace=False, ) def wrap_paragraphs(paragraphs: Iterable[str]) -> str: """Wrap and concatenate paragraphs. Take an iterable of paragraphs as input. Return a string where the paragraphs are concatenated (empty line as separator) and wrapped. End the string in a newline. """ terminal_width, _ = shutil.get_terminal_size() if 0 < terminal_width < 80: wrap_width = terminal_width else: wrap_width = 80 wrapper = cached_textwrapper(wrap_width) return "\n\n".join(wrapper.fill(p) for p in paragraphs) + "\n" @contextlib.contextmanager def log_handler_applied( logger: logging.Logger, handler: logging.Handler ) -> Generator[None, None, None]: logger.addHandler(handler) try: yield finally: logger.removeHandler(handler) def get_plugin_info_str( parser_extension_dists: Mapping[str, tuple[str, list[str]]], codeformatter_dists: Mapping[str, tuple[str, list[str]]], ) -> str: info = "" if codeformatter_dists: info += "installed codeformatters:" for dist, dist_info in codeformatter_dists.items(): langs = ", ".join(dist_info[1]) info += f"\n {dist}: {langs}" if parser_extension_dists: if info: info += "\n\n" info += "installed extensions:" for dist, dist_info in parser_extension_dists.items(): extensions = ", ".join(dist_info[1]) info += f"\n {dist}: {extensions}" return info def get_plugin_version_str(dist_map: Mapping[str, tuple[str, list[str]]]) -> str: return ", ".join( f"{dist_name} {dist_info[0]}" for dist_name, dist_info in dist_map.items() ) def get_source_file_and_line(obj: object) -> tuple[str, int]: import inspect try: filename = inspect.getsourcefile(obj) # type: ignore[arg-type] if filename is None: # pragma: no cover filename = "not found" except TypeError: # pragma: no cover filename = "built-in object" try: _, lineno = inspect.getsourcelines(obj) # type: ignore[arg-type] except (OSError, TypeError): # pragma: no cover lineno = 0 return filename, lineno mdformat-0.7.22/src/mdformat/_compat.py000066400000000000000000000005521475015777400200320ustar00rootroot00000000000000__all__ = ("importlib_metadata", "tomllib") import sys if sys.version_info >= (3, 11): # pragma: >=3.11 cover import tomllib else: # pragma: <3.11 cover import tomli as tomllib if sys.version_info >= (3, 10): # pragma: >=3.10 cover from importlib import metadata as importlib_metadata else: # pragma: <3.10 cover import importlib_metadata mdformat-0.7.22/src/mdformat/_conf.py000066400000000000000000000072751475015777400175050ustar00rootroot00000000000000from __future__ import annotations import functools from pathlib import Path from types import MappingProxyType from typing import Mapping from mdformat._compat import tomllib from mdformat._util import EMPTY_MAP DEFAULT_OPTS = MappingProxyType( { "wrap": "keep", "number": False, "end_of_line": "lf", "validate": True, "exclude": (), "plugin": EMPTY_MAP, "extensions": None, "codeformatters": None, } ) class InvalidConfError(Exception): """Error raised on invalid TOML configuration. Will be raised on: - invalid TOML - invalid conf key - invalid conf value """ @functools.lru_cache() def read_toml_opts(conf_dir: Path) -> tuple[Mapping, Path | None]: conf_path = conf_dir / ".mdformat.toml" if not conf_path.is_file(): parent_dir = conf_dir.parent if conf_dir == parent_dir: return {}, None return read_toml_opts(parent_dir) with open(conf_path, "rb") as f: try: toml_opts = tomllib.load(f) except tomllib.TOMLDecodeError as e: raise InvalidConfError(f"Invalid TOML syntax: {e}") _validate_keys(toml_opts, conf_path) _validate_values(toml_opts, conf_path) return toml_opts, conf_path def _validate_values(opts: Mapping, conf_path: Path) -> None: # noqa: C901 if "wrap" in opts: wrap_value = opts["wrap"] if not ( (isinstance(wrap_value, int) and wrap_value > 1) or wrap_value in {"keep", "no"} ): raise InvalidConfError(f"Invalid 'wrap' value in {conf_path}") if "end_of_line" in opts: if opts["end_of_line"] not in {"crlf", "lf", "keep"}: raise InvalidConfError(f"Invalid 'end_of_line' value in {conf_path}") if "validate" in opts: if not isinstance(opts["validate"], bool): raise InvalidConfError(f"Invalid 'validate' value in {conf_path}") if "number" in opts: if not isinstance(opts["number"], bool): raise InvalidConfError(f"Invalid 'number' value in {conf_path}") if "exclude" in opts: # pragma: >=3.13 cover if not isinstance(opts["exclude"], list): raise InvalidConfError(f"Invalid 'exclude' value in {conf_path}") for pattern in opts["exclude"]: if not isinstance(pattern, str): raise InvalidConfError(f"Invalid 'exclude' value in {conf_path}") if "plugin" in opts: if not isinstance(opts["plugin"], dict): raise InvalidConfError(f"Invalid 'plugin' value in {conf_path}") for plugin_conf in opts["plugin"].values(): if not isinstance(plugin_conf, dict): raise InvalidConfError(f"Invalid 'plugin' value in {conf_path}") if "extensions" in opts: if not isinstance(opts["extensions"], list): raise InvalidConfError(f"Invalid 'extensions' value in {conf_path}") for extension in opts["extensions"]: if not isinstance(extension, str): raise InvalidConfError(f"Invalid 'extensions' value in {conf_path}") if "codeformatters" in opts: if not isinstance(opts["codeformatters"], list): raise InvalidConfError(f"Invalid 'codeformatters' value in {conf_path}") for lang in opts["codeformatters"]: if not isinstance(lang, str): raise InvalidConfError(f"Invalid 'codeformatters' value in {conf_path}") def _validate_keys(opts: Mapping, conf_path: Path) -> None: for key in opts: if key not in DEFAULT_OPTS: raise InvalidConfError( f"Invalid key {key!r} in {conf_path}." f" Keys must be one of {set(DEFAULT_OPTS)}." ) mdformat-0.7.22/src/mdformat/_util.py000066400000000000000000000107661475015777400175340ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable, Mapping from contextlib import nullcontext import re from types import MappingProxyType from typing import TYPE_CHECKING, Any, Literal import mdformat.plugins if TYPE_CHECKING: from markdown_it import MarkdownIt NULL_CTX = nullcontext() EMPTY_MAP: MappingProxyType = MappingProxyType({}) RE_NEWLINES = re.compile(r"\r\n|\r|\n") RE_HTML_START_SPACE_PREFIX = re.compile(r" (<[a-zA-Z][-a-zA-Z0-9]*>)") RE_HTML_END_SPACE_SUFFIX = re.compile(r"() ") def build_mdit( renderer_cls: Any, *, mdformat_opts: Mapping[str, Any] = EMPTY_MAP, extensions: Iterable[str] = (), codeformatters: Iterable[str] = (), ) -> MarkdownIt: # Lazy import to improve module import time from markdown_it import MarkdownIt mdit = MarkdownIt(renderer_cls=renderer_cls) mdit.options["mdformat"] = mdformat_opts # store reference labels in link/image tokens mdit.options["store_labels"] = True mdit.options["parser_extension"] = [] for name in extensions: plugin = mdformat.plugins.PARSER_EXTENSIONS[name] if plugin not in mdit.options["parser_extension"]: mdit.options["parser_extension"].append(plugin) plugin.update_mdit(mdit) mdit.options["codeformatters"] = { lang: mdformat.plugins.CODEFORMATTERS[lang] for lang in codeformatters } return mdit # Chars that markdown-it-py escapes when rendering code_inline: # https://github.com/executablebooks/markdown-it-py/blob/c5161b550f3c6c0a98d77e8389872405e8f9f9ee/markdown_it/common/utils.py#L138 # Note that "&" is not included as it is used in the escape sequences of # these characters. _invalid_html_code_chars = '<>"' # a regex str that matches all except above chars _valid_html_code_char_re = rf"[^{re.escape(_invalid_html_code_chars)}]" def is_md_equal( md1: str, md2: str, *, options: Mapping[str, Any] = EMPTY_MAP, extensions: Iterable[str] = (), codeformatters: Iterable[str] = (), ) -> bool: """Check if two Markdown produce the same HTML. Renders HTML from both Markdown strings, reduces consecutive whitespace to a single space and checks equality. Note that this is not a perfect solution, as there can be meaningful whitespace in HTML, e.g. in a block. """ # Lazy import to improve module import time from markdown_it.renderer import RendererHTML html_texts = {} mdit = build_mdit(RendererHTML, mdformat_opts=options, extensions=extensions) for key, text in [("md1", md1), ("md2", md2)]: html = mdit.render(text) # Remove codeblocks because code formatter plugins do arbitrary changes. if codeformatters: langs_re = "|".join(re.escape(lang) for lang in codeformatters) html = re.sub( rf'' rf"{_valid_html_code_char_re}*" r"", "", html, ) # Reduce all whitespace to a single space html = re.sub(r"\s+", " ", html) # Strip insignificant paragraph leading/trailing whitespace html = html.replace("

", "

") html = html.replace("

", "

") # Also remove whitespace preceding opening tags, and trailing # closing tags, so that we can safely remove empty paragraphs # below without introducing extra whitespace. html = RE_HTML_END_SPACE_SUFFIX.sub(r"\g<1>", html) html = RE_HTML_START_SPACE_PREFIX.sub(r"\g<1>", html) # empty p elements should be ignored by user agents # (https://www.w3.org/TR/REC-html40/struct/text.html#edef-P) html = html.replace("

", "") # Leading and trailing whitespace should be safe to ignore. This # also makes any documents that are whitespace-only equal. html = html.strip() html_texts[key] = html return html_texts["md1"] == html_texts["md2"] def detect_newline_type(md: str, eol_setting: str) -> Literal["\n", "\r\n"]: """Returns the newline-character to be used for output. If `eol_setting == "keep"`, the newline character used in the passed markdown is detected and returned. Otherwise the character matching the passed setting is returned. """ if eol_setting == "keep": first_eol = RE_NEWLINES.search(md) return "\r\n" if first_eol and first_eol.group() == "\r\n" else "\n" if eol_setting == "crlf": return "\r\n" return "\n" mdformat-0.7.22/src/mdformat/codepoints/000077500000000000000000000000001475015777400202035ustar00rootroot00000000000000mdformat-0.7.22/src/mdformat/codepoints/__init__.py000066400000000000000000000016571475015777400223250ustar00rootroot00000000000000__all__ = ( "UNICODE_PUNCTUATION", "UNICODE_WHITESPACE", "ASCII_CTRL", "ASCII_WHITESPACE", ) from mdformat.codepoints._unicode_punctuation import UNICODE_PUNCTUATION from mdformat.codepoints._unicode_whitespace import UNICODE_WHITESPACE ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) def __getattr__(name: str) -> frozenset[str]: """Attribute getter fallback. Used during the deprecation period of `ASCII_WHITESPACE`. """ if name == "ASCII_WHITESPACE": import warnings warnings.warn( "ASCII_WHITESPACE is deprecated because CommonMark v0.30 no longer " "defines ASCII whitespace.", DeprecationWarning, stacklevel=2, ) return frozenset({chr(9), chr(10), chr(11), chr(12), chr(13), chr(32)}) raise AttributeError( f"module {__name__!r} has no attribute {name!r}" ) # pragma: no cover mdformat-0.7.22/src/mdformat/codepoints/_unicode_punctuation.py000066400000000000000000000305221475015777400247750ustar00rootroot00000000000000"""Pre-generated unicode punctuation characters. Run this module to generate and print an up-to-date set of characters. """ UNICODE_PUNCTUATION = frozenset( ( "!", '"', "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~", "¡", "§", "«", "¶", "·", "»", "¿", ";", "·", "՚", "՛", "՜", "՝", "՞", "՟", "։", "֊", "־", "׀", "׃", "׆", "׳", "״", "؉", "؊", "،", "؍", "؛", "؞", "؟", "٪", "٫", "٬", "٭", "۔", "܀", "܁", "܂", "܃", "܄", "܅", "܆", "܇", "܈", "܉", "܊", "܋", "܌", "܍", "߷", "߸", "߹", "࠰", "࠱", "࠲", "࠳", "࠴", "࠵", "࠶", "࠷", "࠸", "࠹", "࠺", "࠻", "࠼", "࠽", "࠾", "࡞", "।", "॥", "॰", "৽", "੶", "૰", "౷", "಄", "෴", "๏", "๚", "๛", "༄", "༅", "༆", "༇", "༈", "༉", "༊", "་", "༌", "།", "༎", "༏", "༐", "༑", "༒", "༔", "༺", "༻", "༼", "༽", "྅", "࿐", "࿑", "࿒", "࿓", "࿔", "࿙", "࿚", "၊", "။", "၌", "၍", "၎", "၏", "჻", "፠", "፡", "።", "፣", "፤", "፥", "፦", "፧", "፨", "᐀", "᙮", "᚛", "᚜", "᛫", "᛬", "᛭", "᜵", "᜶", "។", "៕", "៖", "៘", "៙", "៚", "᠀", "᠁", "᠂", "᠃", "᠄", "᠅", "᠆", "᠇", "᠈", "᠉", "᠊", "᥄", "᥅", "᨞", "᨟", "᪠", "᪡", "᪢", "᪣", "᪤", "᪥", "᪦", "᪨", "᪩", "᪪", "᪫", "᪬", "᪭", "᭚", "᭛", "᭜", "᭝", "᭞", "᭟", "᭠", "᯼", "᯽", "᯾", "᯿", "᰻", "᰼", "᰽", "᰾", "᰿", "᱾", "᱿", "᳀", "᳁", "᳂", "᳃", "᳄", "᳅", "᳆", "᳇", "᳓", "‐", "‑", "‒", "–", "—", "―", "‖", "‗", "‘", "’", "‚", "‛", "“", "”", "„", "‟", "†", "‡", "•", "‣", "․", "‥", "…", "‧", "‰", "‱", "′", "″", "‴", "‵", "‶", "‷", "‸", "‹", "›", "※", "‼", "‽", "‾", "‿", "⁀", "⁁", "⁂", "⁃", "⁅", "⁆", "⁇", "⁈", "⁉", "⁊", "⁋", "⁌", "⁍", "⁎", "⁏", "⁐", "⁑", "⁓", "⁔", "⁕", "⁖", "⁗", "⁘", "⁙", "⁚", "⁛", "⁜", "⁝", "⁞", "⁽", "⁾", "₍", "₎", "⌈", "⌉", "⌊", "⌋", "〈", "〉", "❨", "❩", "❪", "❫", "❬", "❭", "❮", "❯", "❰", "❱", "❲", "❳", "❴", "❵", "⟅", "⟆", "⟦", "⟧", "⟨", "⟩", "⟪", "⟫", "⟬", "⟭", "⟮", "⟯", "⦃", "⦄", "⦅", "⦆", "⦇", "⦈", "⦉", "⦊", "⦋", "⦌", "⦍", "⦎", "⦏", "⦐", "⦑", "⦒", "⦓", "⦔", "⦕", "⦖", "⦗", "⦘", "⧘", "⧙", "⧚", "⧛", "⧼", "⧽", "⳹", "⳺", "⳻", "⳼", "⳾", "⳿", "⵰", "⸀", "⸁", "⸂", "⸃", "⸄", "⸅", "⸆", "⸇", "⸈", "⸉", "⸊", "⸋", "⸌", "⸍", "⸎", "⸏", "⸐", "⸑", "⸒", "⸓", "⸔", "⸕", "⸖", "⸗", "⸘", "⸙", "⸚", "⸛", "⸜", "⸝", "⸞", "⸟", "⸠", "⸡", "⸢", "⸣", "⸤", "⸥", "⸦", "⸧", "⸨", "⸩", "⸪", "⸫", "⸬", "⸭", "⸮", "⸰", "⸱", "⸲", "⸳", "⸴", "⸵", "⸶", "⸷", "⸸", "⸹", "⸺", "⸻", "⸼", "⸽", "⸾", "⸿", "⹀", "⹁", "⹂", "⹃", "⹄", "⹅", "⹆", "⹇", "⹈", "⹉", "⹊", "⹋", "⹌", "⹍", "⹎", "⹏", "、", "。", "〃", "〈", "〉", "《", "》", "「", "」", "『", "』", "【", "】", "〔", "〕", "〖", "〗", "〘", "〙", "〚", "〛", "〜", "〝", "〞", "〟", "〰", "〽", "゠", "・", "꓾", "꓿", "꘍", "꘎", "꘏", "꙳", "꙾", "꛲", "꛳", "꛴", "꛵", "꛶", "꛷", "꡴", "꡵", "꡶", "꡷", "꣎", "꣏", "꣸", "꣹", "꣺", "꣼", "꤮", "꤯", "꥟", "꧁", "꧂", "꧃", "꧄", "꧅", "꧆", "꧇", "꧈", "꧉", "꧊", "꧋", "꧌", "꧍", "꧞", "꧟", "꩜", "꩝", "꩞", "꩟", "꫞", "꫟", "꫰", "꫱", "꯫", "﴾", "﴿", "︐", "︑", "︒", "︓", "︔", "︕", "︖", "︗", "︘", "︙", "︰", "︱", "︲", "︳", "︴", "︵", "︶", "︷", "︸", "︹", "︺", "︻", "︼", "︽", "︾", "︿", "﹀", "﹁", "﹂", "﹃", "﹄", "﹅", "﹆", "﹇", "﹈", "﹉", "﹊", "﹋", "﹌", "﹍", "﹎", "﹏", "﹐", "﹑", "﹒", "﹔", "﹕", "﹖", "﹗", "﹘", "﹙", "﹚", "﹛", "﹜", "﹝", "﹞", "﹟", "﹠", "﹡", "﹣", "﹨", "﹪", "﹫", "!", """, "#", "%", "&", "'", "(", ")", "*", ",", "-", ".", "/", ":", ";", "?", "@", "[", "\", "]", "_", "{", "}", "⦅", "⦆", "。", "「", "」", "、", "・", "𐄀", "𐄁", "𐄂", "𐎟", "𐏐", "𐕯", "𐡗", "𐤟", "𐤿", "𐩐", "𐩑", "𐩒", "𐩓", "𐩔", "𐩕", "𐩖", "𐩗", "𐩘", "𐩿", "𐫰", "𐫱", "𐫲", "𐫳", "𐫴", "𐫵", "𐫶", "𐬹", "𐬺", "𐬻", "𐬼", "𐬽", "𐬾", "𐬿", "𐮙", "𐮚", "𐮛", "𐮜", "𐽕", "𐽖", "𐽗", "𐽘", "𐽙", "𑁇", "𑁈", "𑁉", "𑁊", "𑁋", "𑁌", "𑁍", "𑂻", "𑂼", "𑂾", "𑂿", "𑃀", "𑃁", "𑅀", "𑅁", "𑅂", "𑅃", "𑅴", "𑅵", "𑇅", "𑇆", "𑇇", "𑇈", "𑇍", "𑇛", "𑇝", "𑇞", "𑇟", "𑈸", "𑈹", "𑈺", "𑈻", "𑈼", "𑈽", "𑊩", "𑑋", "𑑌", "𑑍", "𑑎", "𑑏", "𑑛", "𑑝", "𑓆", "𑗁", "𑗂", "𑗃", "𑗄", "𑗅", "𑗆", "𑗇", "𑗈", "𑗉", "𑗊", "𑗋", "𑗌", "𑗍", "𑗎", "𑗏", "𑗐", "𑗑", "𑗒", "𑗓", "𑗔", "𑗕", "𑗖", "𑗗", "𑙁", "𑙂", "𑙃", "𑙠", "𑙡", "𑙢", "𑙣", "𑙤", "𑙥", "𑙦", "𑙧", "𑙨", "𑙩", "𑙪", "𑙫", "𑙬", "𑜼", "𑜽", "𑜾", "𑠻", "𑧢", "𑨿", "𑩀", "𑩁", "𑩂", "𑩃", "𑩄", "𑩅", "𑩆", "𑪚", "𑪛", "𑪜", "𑪞", "𑪟", "𑪠", "𑪡", "𑪢", "𑱁", "𑱂", "𑱃", "𑱄", "𑱅", "𑱰", "𑱱", "𑻷", "𑻸", "𑿿", "𒑰", "𒑱", "𒑲", "𒑳", "𒑴", "𖩮", "𖩯", "𖫵", "𖬷", "𖬸", "𖬹", "𖬺", "𖬻", "𖭄", "𖺗", "𖺘", "𖺙", "𖺚", "𖿢", "𛲟", "𝪇", "𝪈", "𝪉", "𝪊", "𝪋", "𞥞", "𞥟", ) ) if __name__ == "__main__": import string import sys import unicodedata UNICODE_CHARS = frozenset(chr(c) for c in range(sys.maxunicode + 1)) UNICODE_PUNCTUATION = frozenset( c for c in UNICODE_CHARS if unicodedata.category(c).startswith("P") ) | frozenset(string.punctuation) print(f"frozenset({tuple(sorted(UNICODE_PUNCTUATION))})") mdformat-0.7.22/src/mdformat/codepoints/_unicode_whitespace.py000066400000000000000000000016101475015777400245540ustar00rootroot00000000000000"""Pre-generated unicode whitespace characters. Run this module to generate and print an up-to-date set of characters. """ UNICODE_WHITESPACE = frozenset( ( "\t", "\n", "\x0b", "\x0c", "\r", " ", "\xa0", "\u1680", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006", "\u2007", "\u2008", "\u2009", "\u200a", "\u202f", "\u205f", "\u3000", ) ) if __name__ == "__main__": import string import sys import unicodedata UNICODE_CHARS = frozenset(chr(c) for c in range(sys.maxunicode + 1)) UNICODE_WHITESPACE = frozenset( c for c in UNICODE_CHARS if unicodedata.category(c) == "Zs" ) | frozenset(string.whitespace) print(f"frozenset({tuple(sorted(UNICODE_WHITESPACE))})") mdformat-0.7.22/src/mdformat/plugins.py000066400000000000000000000107301475015777400200700ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Protocol from mdformat._compat import importlib_metadata if TYPE_CHECKING: import argparse from collections.abc import Callable, Mapping from markdown_it import MarkdownIt from mdformat.renderer.typing import Postprocess, Render def _load_entrypoints( eps: importlib_metadata.EntryPoints, ) -> tuple[dict[str, Any], dict[str, tuple[str, list[str]]]]: loaded_ifaces: dict[str, Any] = {} dist_versions: dict[str, tuple[str, list[str]]] = {} for ep in eps: assert ep.dist, ( "EntryPoint.dist should never be None " "when coming from Distribution.entry_points" ) loaded_ifaces[ep.name] = ep.load() dist_name = ep.dist.name if dist_name in dist_versions: dist_versions[dist_name][1].append(ep.name) else: dist_versions[dist_name] = (ep.dist.version, [ep.name]) return loaded_ifaces, dist_versions class ParserExtensionInterface(Protocol): """An interface for parser extension plugins.""" # Does the plugin's formatting change Markdown AST or not? # (optional, default: False) CHANGES_AST: bool = False # A mapping from `RenderTreeNode.type` to a `Render` function that can # render the given `RenderTreeNode` type. These override the default # `Render` funcs defined in `mdformat.renderer.DEFAULT_RENDERERS`. RENDERERS: Mapping[str, Render] # A mapping from `RenderTreeNode.type` to a `Postprocess` that does # postprocessing for the output of the `Render` function. Unlike # `Render` funcs, `Postprocess` funcs are collaborative: any number of # plugins can define a postprocessor for a syntax type and all of them # will run in series. # (optional) POSTPROCESSORS: Mapping[str, Postprocess] @staticmethod def add_cli_options(parser: argparse.ArgumentParser) -> None: """DEPRECATED - use `add_cli_argument_group` instead. Add options to the mdformat CLI, to be stored in mdit.options["mdformat"] (optional) """ @staticmethod def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: """Add an argument group to mdformat CLI and add arguments to it. Call `group.add_argument()` to add CLI arguments (signature is the same as argparse.ArgumentParser.add_argument). Values will be stored in a mapping under mdit.options["mdformat"]["plugin"][] where equals entry point name of the plugin. The mapping will be merged with values read from TOML config file section [plugin.]. (optional) """ @staticmethod def update_mdit(mdit: MarkdownIt) -> None: """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" CODEFORMATTERS: Mapping[str, Callable[[str, str], str]] _CODEFORMATTER_DISTS: Mapping[str, tuple[str, list[str]]] PARSER_EXTENSIONS: Mapping[str, ParserExtensionInterface] _PARSER_EXTENSION_DISTS: Mapping[str, tuple[str, list[str]]] def __getattr__(name: str) -> Mapping[str, Any]: """Attribute getter fallback. Used to lazy load CODEFORMATTERS and PARSER_EXTENSIONS. It'd probably be more readable to use `@functools.cache` decorated functions, but `__getattr__` is used now for back compatibility. """ if name in {"CODEFORMATTERS", "_CODEFORMATTER_DISTS"}: formatters, formatter_dists = _load_entrypoints( importlib_metadata.entry_points(group="mdformat.codeformatter") ) # Cache the values in this module for next time, so that `__getattr__` # is only called once per `name`. global CODEFORMATTERS, _CODEFORMATTER_DISTS CODEFORMATTERS = formatters _CODEFORMATTER_DISTS = formatter_dists return formatters if name == "CODEFORMATTERS" else formatter_dists if name in {"PARSER_EXTENSIONS", "_PARSER_EXTENSION_DISTS"}: extensions, extension_dists = _load_entrypoints( importlib_metadata.entry_points(group="mdformat.parser_extension") ) # Cache the value in this module for next time, so that `__getattr__` # is only called once per `name`. global PARSER_EXTENSIONS, _PARSER_EXTENSION_DISTS PARSER_EXTENSIONS = extensions _PARSER_EXTENSION_DISTS = extension_dists return extensions if name == "PARSER_EXTENSIONS" else extension_dists raise AttributeError(f"module {__name__!r} has no attribute {name!r}") mdformat-0.7.22/src/mdformat/py.typed000066400000000000000000000000321475015777400175260ustar00rootroot00000000000000# Marker file for PEP 561 mdformat-0.7.22/src/mdformat/renderer/000077500000000000000000000000001475015777400176425ustar00rootroot00000000000000mdformat-0.7.22/src/mdformat/renderer/__init__.py000066400000000000000000000101601475015777400217510ustar00rootroot00000000000000from __future__ import annotations __all__ = ( "MDRenderer", "LOGGER", "RenderTreeNode", "DEFAULT_RENDERERS", "RenderContext", "WRAP_POINT", ) from collections.abc import Mapping, MutableMapping, Sequence import logging import string from types import MappingProxyType from typing import TYPE_CHECKING, Any from mdformat.renderer._context import DEFAULT_RENDERERS, WRAP_POINT, RenderContext from mdformat.renderer._tree import RenderTreeNode if TYPE_CHECKING: from markdown_it.token import Token from mdformat.renderer.typing import Postprocess LOGGER = logging.getLogger(__name__) class MDRenderer: """Markdown renderer. A renderer class that outputs formatted Markdown. Compatible with `markdown_it.MarkdownIt`. """ __output__ = "md" def __init__(self, parser: Any = None): """__init__ must have `parser` parameter for markdown-it-py compatibility.""" def render( self, tokens: Sequence[Token], options: Mapping[str, Any], env: MutableMapping, *, finalize: bool = True, ) -> str: """Takes token stream and generates Markdown. Args: tokens: A sequence of block tokens to render options: Params of parser instance env: Additional data from parsed input finalize: write references and add trailing newline """ tree = RenderTreeNode(tokens) return self.render_tree(tree, options, env, finalize=finalize) def render_tree( self, tree: RenderTreeNode, options: Mapping[str, Any], env: MutableMapping, *, finalize: bool = True, ) -> str: self._prepare_env(env) # Update RENDERER_MAP defaults with renderer functions defined # by plugins. updated_renderers = {} postprocessors: dict[str, tuple[Postprocess, ...]] = {} for plugin in options.get("parser_extension", []): for syntax_name, renderer_func in plugin.RENDERERS.items(): if syntax_name in updated_renderers: LOGGER.warning( "Plugin conflict. More than one plugin defined a renderer" f' for "{syntax_name}" syntax.' ) else: updated_renderers[syntax_name] = renderer_func for syntax_name, pp in getattr(plugin, "POSTPROCESSORS", {}).items(): if syntax_name not in postprocessors: postprocessors[syntax_name] = (pp,) else: postprocessors[syntax_name] += (pp,) renderer_map = MappingProxyType({**DEFAULT_RENDERERS, **updated_renderers}) postprocessor_map = MappingProxyType(postprocessors) render_context = RenderContext(renderer_map, postprocessor_map, options, env) text = tree.render(render_context) if finalize: if env["used_refs"]: text += "\n\n" text += self._write_references(env) if text: text += "\n" assert "\x00" not in text, "null bytes should be removed by now" return text @staticmethod def _write_references(env: MutableMapping) -> str: def label_sort_key(label: str) -> str: assert label, "link label cannot be empty string" if all(c in string.digits for c in label): label_max_len = 999 # This is from CommonMark v0.30 return label.rjust(label_max_len, "0") return label ref_list = [] for label in sorted(env["used_refs"], key=label_sort_key): ref = env["references"][label] destination = ref["href"] if ref["href"] else "<>" item = f"[{label.lower()}]: {destination}" title = ref["title"] if title: title = title.replace('"', '\\"') item += f' "{title}"' ref_list.append(item) return "\n".join(ref_list) def _prepare_env(self, env: MutableMapping) -> None: env["indent_width"] = 0 env["used_refs"] = set() mdformat-0.7.22/src/mdformat/renderer/_context.py000066400000000000000000000560011475015777400220410ustar00rootroot00000000000000from __future__ import annotations from collections import defaultdict from collections.abc import Generator, Iterable, Mapping, MutableMapping from contextlib import contextmanager import functools import logging import re import textwrap from types import MappingProxyType from typing import TYPE_CHECKING, Any, Literal, NamedTuple from markdown_it.rules_block.html_block import HTML_SEQUENCES from mdformat import codepoints from mdformat._conf import DEFAULT_OPTS from mdformat.renderer._util import ( decimalify_leading, decimalify_trailing, escape_asterisk_emphasis, escape_less_than_sign, escape_square_brackets, escape_underscore_emphasis, get_list_marker_type, is_tight_list, is_tight_list_item, longest_consecutive_sequence, maybe_add_link_brackets, re_char_reference, ) if TYPE_CHECKING: from mdformat.renderer import RenderTreeNode from mdformat.renderer.typing import Postprocess, Render LOGGER = logging.getLogger(__name__) # A marker used to point a location where word wrap is allowed # to occur. WRAP_POINT = "\x00" # A marker used to indicate location of a character that should be preserved # during word wrap. Should be converted to the actual character after wrap. PRESERVE_CHAR = "\x00" RE_PRESERVE_CHAR = re.compile(re.escape(PRESERVE_CHAR)) RE_UNICODE_WS_OR_WRAP_POINT = re.compile( rf"[{re.escape(''.join(codepoints.UNICODE_WHITESPACE))}]" "|" rf"{re.escape(WRAP_POINT)}+" ) def make_render_children(separator: str) -> Render: def render_children( node: RenderTreeNode, context: RenderContext, ) -> str: render_outputs = (child.render(context) for child in node.children) return separator.join(out for out in render_outputs if out) return render_children def hr(node: RenderTreeNode, context: RenderContext) -> str: thematic_break_width = 70 return "_" * thematic_break_width def code_inline(node: RenderTreeNode, context: RenderContext) -> str: code = node.content all_chars_are_whitespace = not code.strip() longest_backtick_seq = longest_consecutive_sequence(code, "`") if longest_backtick_seq: separator = "`" * (longest_backtick_seq + 1) return f"{separator} {code} {separator}" if code.startswith(" ") and code.endswith(" ") and not all_chars_are_whitespace: return f"` {code} `" return f"`{code}`" def html_block(node: RenderTreeNode, context: RenderContext) -> str: content = node.content.rstrip("\n") # Need to strip leading spaces because we do so for regular Markdown too. # Without the stripping the raw HTML and Markdown get unaligned and # semantic may change. content = content.lstrip() return content def html_inline(node: RenderTreeNode, context: RenderContext) -> str: return node.content def _in_block(block_name: str, node: RenderTreeNode) -> bool: while node.parent: if node.parent.type == block_name: return True node = node.parent return False def hardbreak(node: RenderTreeNode, context: RenderContext) -> str: if _in_block("heading", node): return "
" return "\\" + "\n" def softbreak(node: RenderTreeNode, context: RenderContext) -> str: if context.do_wrap and _in_block("paragraph", node): return WRAP_POINT return "\n" def text(node: RenderTreeNode, context: RenderContext) -> str: """Process a text token. Text should always be a child of an inline token. An inline token should always be enclosed by a heading or a paragraph. """ text = node.content # Convert tabs to spaces text = text.replace("\t", " ") # Reduce tabs and spaces to one space text = re.sub(" {2,}", " ", text) # Escape backslash to prevent it from making unintended escapes. # This escape has to be first, else we start multiplying backslashes. text = text.replace("\\", "\\\\") text = escape_asterisk_emphasis(text) # Escape emphasis/strong marker. text = escape_underscore_emphasis(text) # Escape emphasis/strong marker. # Escape link label and link ref enclosures text = escape_square_brackets(text, context.env["used_refs"]) text = escape_less_than_sign(text) # Escape URI enclosure and HTML. text = text.replace("`", "\\`") # Escape code span marker # Escape "&" if it starts a sequence that can be interpreted as # a character reference. text = re_char_reference().sub(r"\\\g<0>", text) # The parser can give us consecutive newlines which can break # the markdown structure. Replace two or more consecutive newlines # with newline character's decimal reference. text = text.replace("\n\n", " ") # If the last character is a "!" and the token next up is a link, we # have to escape the "!" or else the link will be interpreted as image. next_sibling = node.next_sibling if text.endswith("!") and next_sibling and next_sibling.type == "link": text = text[:-1] + "\\!" if context.do_wrap and _in_block("paragraph", node): text = re.sub(r"[ \t\n]+", WRAP_POINT, text) return text def fence(node: RenderTreeNode, context: RenderContext) -> str: info_str = node.info.strip() lang = info_str.split(maxsplit=1)[0] if info_str else "" code_block = node.content # Info strings of backtick code fences cannot contain backticks. # If that is the case, we make a tilde code fence instead. fence_char = "~" if "`" in info_str else "`" # Format the code block using enabled codeformatter funcs fmt_func = context.options.get("codeformatters", {}).get(lang) if fmt_func: try: code_block = fmt_func(code_block, info_str) except Exception: # Swallow exceptions so that formatter errors (e.g. due to # invalid code) do not crash mdformat. assert node.map is not None, "A fence token must have `map` attribute set" filename = context.options.get("mdformat", {}).get("filename", "") warn_msg = ( f"Failed formatting content of a {lang} code block " f"(line {node.map[0] + 1} before formatting)" ) if filename: warn_msg += f". Filename: {filename}" LOGGER.warning(warn_msg) else: if code_block and code_block[-1] != "\n": code_block += "\n" # The code block must not include as long or longer sequence of `fence_char`s # as the fence string itself fence_len = max(3, longest_consecutive_sequence(code_block, fence_char) + 1) fence_str = fence_char * fence_len return f"{fence_str}{info_str}\n{code_block}{fence_str}" def code_block(node: RenderTreeNode, context: RenderContext) -> str: return fence(node, context) def image(node: RenderTreeNode, context: RenderContext) -> str: description = _render_inline_as_text(node, context) if context.do_wrap: # Prevent line breaks description = description.replace(WRAP_POINT, " ") ref_label = node.meta.get("label") if ref_label: context.env["used_refs"].add(ref_label) ref_label_repr = ref_label.lower() if description.lower() == ref_label_repr: return f"![{description}]" return f"![{description}][{ref_label_repr}]" uri = node.attrs["src"] assert isinstance(uri, str) uri = maybe_add_link_brackets(uri) title = node.attrs.get("title") if title is not None: return f'![{description}]({uri} "{title}")' return f"![{description}]({uri})" def _render_inline_as_text(node: RenderTreeNode, context: RenderContext) -> str: """Special kludge for image `alt` attributes to conform CommonMark spec. Don't try to use it! Spec requires to show `alt` content with stripped markup, instead of simple escaping. """ def text_renderer(node: RenderTreeNode, context: RenderContext) -> str: return node.content def image_renderer(node: RenderTreeNode, context: RenderContext) -> str: return _render_inline_as_text(node, context) inline_renderers: Mapping[str, Render] = defaultdict( lambda: make_render_children(""), { "text": text_renderer, "image": image_renderer, "link": link, "softbreak": softbreak, }, ) inline_context = RenderContext( inline_renderers, context.postprocessors, context.options, context.env ) return make_render_children("")(node, inline_context) def link(node: RenderTreeNode, context: RenderContext) -> str: if node.info == "auto": autolink_url = node.attrs["href"] assert isinstance(autolink_url, str) # The parser adds a "mailto:" prefix to autolink email href. We remove the # prefix if it wasn't there in the source. if autolink_url.startswith("mailto:") and not node.children[ 0 ].content.startswith("mailto:"): autolink_url = autolink_url[7:] return "<" + autolink_url + ">" text = "".join(child.render(context) for child in node.children) if context.do_wrap: # Prevent line breaks text = text.replace(WRAP_POINT, " ") ref_label = node.meta.get("label") if ref_label: context.env["used_refs"].add(ref_label) ref_label_repr = ref_label.lower() if text.lower() == ref_label_repr: return f"[{text}]" return f"[{text}][{ref_label_repr}]" uri = node.attrs["href"] assert isinstance(uri, str) uri = maybe_add_link_brackets(uri) title = node.attrs.get("title") if title is None: return f"[{text}]({uri})" assert isinstance(title, str) title = title.replace('"', '\\"') return f'[{text}]({uri} "{title}")' def em(node: RenderTreeNode, context: RenderContext) -> str: text = make_render_children(separator="")(node, context) indicator = node.markup return indicator + text + indicator def strong(node: RenderTreeNode, context: RenderContext) -> str: text = make_render_children(separator="")(node, context) indicator = node.markup return indicator + text + indicator def heading(node: RenderTreeNode, context: RenderContext) -> str: text = make_render_children(separator="")(node, context) if node.markup == "=": prefix = "# " elif node.markup == "-": prefix = "## " else: # ATX heading prefix = node.markup + " " # There can be newlines in setext headers, but we make an ATX # header always. Convert newlines to spaces. text = text.replace("\n", " ") # If the text ends in a sequence of hashes (#), the hashes will be # interpreted as an optional closing sequence of the heading, and # will not be rendered. Escape a line ending hash to prevent this. if text.endswith("#"): text = text[:-1] + "\\#" return prefix + text def blockquote(node: RenderTreeNode, context: RenderContext) -> str: marker = "> " with context.indented(len(marker)): text = make_render_children(separator="\n\n")(node, context) lines = text.splitlines() if not lines: return ">" quoted_lines = (f"{marker}{line}" if line else ">" for line in lines) quoted_str = "\n".join(quoted_lines) return quoted_str @functools.lru_cache def cached_textwrapper(width: int) -> textwrap.TextWrapper: return textwrap.TextWrapper( break_long_words=False, break_on_hyphens=False, width=width, expand_tabs=False, replace_whitespace=False, ) def _wrap(text: str, *, width: int | Literal["no"]) -> str: """Wrap text at locations pointed by `WRAP_POINT`s. Converts `WRAP_POINT`s to either a space or newline character, thus wrapping the text. Already existing whitespace will be preserved as is. """ text, replacements = _prepare_wrap(text) if width == "no": return _recover_preserve_chars(text, replacements) wrapper = cached_textwrapper(width) wrapped = wrapper.fill(text) wrapped = _recover_preserve_chars(wrapped, replacements) return wrapped def _prepare_wrap(text: str) -> tuple[str, list[str]]: """Prepare text for wrap. Convert `WRAP_POINT`s to spaces. Convert whitespace to `PRESERVE_CHAR`s. Return a tuple with the prepared string, and a list consisting of replacement characters for `PRESERVE_CHAR`s. """ replacements = [] def replacer(match: re.Match[str]) -> str: first_char = match.group()[0] if first_char == WRAP_POINT: return " " replacements.append(first_char) return PRESERVE_CHAR result = RE_UNICODE_WS_OR_WRAP_POINT.sub(replacer, text) return result, replacements def _recover_preserve_chars(text: str, replacements: Iterable[str]) -> str: iter_replacements = iter(replacements) return RE_PRESERVE_CHAR.sub(lambda _: next(iter_replacements), text) def paragraph(node: RenderTreeNode, context: RenderContext) -> str: # noqa: C901 inline_node = node.children[0] text = inline_node.render(context) if context.do_wrap: wrap_mode = context.options["mdformat"]["wrap"] if isinstance(wrap_mode, int): wrap_mode -= context.env["indent_width"] wrap_mode = max(1, wrap_mode) # Newlines should be mostly WRAP_POINTs by now, but there are # exceptional newlines that need to be preserved: # - hard breaks: newline defines the hard break # - html inline: newline vs space can be the difference between # html block and html inline # Split the text and word wrap each section separately. sections = text.split("\n") text = "\n".join(_wrap(s, width=wrap_mode) for s in sections) # A paragraph can start or end in whitespace e.g. if the whitespace was # in decimal representation form. We need to re-decimalify it, one reason being # to enable "empty" paragraphs with whitespace only. text = decimalify_leading(codepoints.UNICODE_WHITESPACE, text) text = decimalify_trailing(codepoints.UNICODE_WHITESPACE, text) lines = text.split("\n") for i in range(len(lines)): # Strip whitespace to prevent issues like a line starting tab that is # interpreted as start of a code block. lines[i] = lines[i].strip() # If a line looks like an ATX heading, escape the first hash. if re.match(r"#{1,6}( |\t|$)", lines[i]): lines[i] = f"\\{lines[i]}" # Make sure a paragraph line does not start with ">" # (otherwise it will be interpreted as a block quote). if lines[i].startswith(">"): lines[i] = f"\\{lines[i]}" # Make sure a paragraph line does not start with "*", "-" or "+" # followed by a space, tab, or end of line. # (otherwise it will be interpreted as list item). if re.match(r"[-*+]( |\t|$)", lines[i]): lines[i] = f"\\{lines[i]}" # If a line starts with a number followed by "." or ")" followed by # a space, tab or end of line, escape the "." or ")" or it will be # interpreted as ordered list item. if re.match(r"[0-9]+\)( |\t|$)", lines[i]): lines[i] = lines[i].replace(")", "\\)", 1) if re.match(r"[0-9]+\.( |\t|$)", lines[i]): lines[i] = lines[i].replace(".", "\\.", 1) # Consecutive "-", "*" or "_" sequences can be interpreted as thematic # break. Escape them. space_removed = lines[i].replace(" ", "").replace("\t", "") if len(space_removed) >= 3: if all(c == "*" for c in space_removed): lines[i] = lines[i].replace("*", "\\*", 1) # pragma: no cover elif all(c == "-" for c in space_removed): lines[i] = lines[i].replace("-", "\\-", 1) elif all(c == "_" for c in space_removed): lines[i] = lines[i].replace("_", "\\_", 1) # pragma: no cover # A stripped line where all characters are "=" or "-" will be # interpreted as a setext heading. Escape. stripped = lines[i].strip(" \t") if all(c == "-" for c in stripped): lines[i] = lines[i].replace("-", "\\-", 1) elif all(c == "=" for c in stripped): lines[i] = lines[i].replace("=", "\\=", 1) # Check if the line could be interpreted as an HTML block. # If yes, prefix it with 4 spaces to prevent this. for html_seq_tuple in HTML_SEQUENCES: can_break_paragraph = html_seq_tuple[2] opening_re = html_seq_tuple[0] if can_break_paragraph and opening_re.search(lines[i]): lines[i] = f" {lines[i]}" break text = "\n".join(lines) return text def list_item(node: RenderTreeNode, context: RenderContext) -> str: """Return one list item as string. This returns just the content. List item markers and indentation are added in `bullet_list` and `ordered_list` renderers. """ block_separator = "\n" if is_tight_list_item(node) else "\n\n" text = make_render_children(block_separator)(node, context) if not text.strip(): return "" return text def bullet_list(node: RenderTreeNode, context: RenderContext) -> str: marker_type = get_list_marker_type(node) first_line_indent = " " indent = " " * len(marker_type + first_line_indent) block_separator = "\n" if is_tight_list(node) else "\n\n" with context.indented(len(indent)): text = "" for child_idx, child in enumerate(node.children): list_item = child.render(context) formatted_lines = [] line_iterator = iter(list_item.split("\n")) first_line = next(line_iterator) formatted_lines.append( f"{marker_type}{first_line_indent}{first_line}" if first_line else marker_type ) for line in line_iterator: formatted_lines.append(f"{indent}{line}" if line else "") text += "\n".join(formatted_lines) if child_idx != len(node.children) - 1: text += block_separator return text def ordered_list(node: RenderTreeNode, context: RenderContext) -> str: consecutive_numbering = context.options.get("mdformat", {}).get( "number", DEFAULT_OPTS["number"] ) marker_type = get_list_marker_type(node) first_line_indent = " " block_separator = "\n" if is_tight_list(node) else "\n\n" list_len = len(node.children) starting_number = node.attrs.get("start") if starting_number is None: starting_number = 1 assert isinstance(starting_number, int) if consecutive_numbering: indent_width = len( f"{list_len + starting_number - 1}{marker_type}{first_line_indent}" ) else: indent_width = len(f"{starting_number}{marker_type}{first_line_indent}") text = "" with context.indented(indent_width): for list_item_index, list_item in enumerate(node.children): list_item_text = list_item.render(context) formatted_lines = [] line_iterator = iter(list_item_text.split("\n")) first_line = next(line_iterator) if consecutive_numbering: # Prefix first line of the list item with consecutive numbering, # padded with zeros to make all markers of even length. # E.g. # 002. This is the first list item # 003. Second item # ... # 112. Last item number = starting_number + list_item_index pad = len(str(list_len + starting_number - 1)) number_str = str(number).rjust(pad, "0") formatted_lines.append( f"{number_str}{marker_type}{first_line_indent}{first_line}" if first_line else f"{number_str}{marker_type}" ) else: # Prefix first line of first item with the starting number of the # list. Prefix following list items with the number one # prefixed by zeros to make the list item marker of even length # with the first one. # E.g. # 5321. This is the first list item # 0001. Second item # 0001. Third item first_item_marker = f"{starting_number}{marker_type}" other_item_marker = ( "0" * (len(str(starting_number)) - 1) + "1" + marker_type ) if list_item_index == 0: formatted_lines.append( f"{first_item_marker}{first_line_indent}{first_line}" if first_line else first_item_marker ) else: formatted_lines.append( f"{other_item_marker}{first_line_indent}{first_line}" if first_line else other_item_marker ) for line in line_iterator: formatted_lines.append(" " * indent_width + line if line else "") text += "\n".join(formatted_lines) if list_item_index != len(node.children) - 1: text += block_separator return text DEFAULT_RENDERERS: Mapping[str, Render] = MappingProxyType( { "inline": make_render_children(""), "root": make_render_children("\n\n"), "hr": hr, "code_inline": code_inline, "html_block": html_block, "html_inline": html_inline, "hardbreak": hardbreak, "softbreak": softbreak, "text": text, "fence": fence, "code_block": code_block, "link": link, "image": image, "em": em, "strong": strong, "heading": heading, "blockquote": blockquote, "paragraph": paragraph, "bullet_list": bullet_list, "ordered_list": ordered_list, "list_item": list_item, } ) class RenderContext(NamedTuple): """A collection of data that is passed as input to `Render` and `Postprocess` functions.""" renderers: Mapping[str, Render] postprocessors: Mapping[str, Iterable[Postprocess]] options: Mapping[str, Any] env: MutableMapping @contextmanager def indented(self, width: int) -> Generator[None, None, None]: self.env["indent_width"] += width try: yield finally: self.env["indent_width"] -= width @property def do_wrap(self) -> bool: wrap_mode = self.options.get("mdformat", {}).get("wrap", DEFAULT_OPTS["wrap"]) return isinstance(wrap_mode, int) or wrap_mode == "no" def with_default_renderer_for(self, *syntax_names: str) -> RenderContext: renderers = dict(self.renderers) for syntax in syntax_names: if syntax in DEFAULT_RENDERERS: renderers[syntax] = DEFAULT_RENDERERS[syntax] else: renderers.pop(syntax, None) return RenderContext( MappingProxyType(renderers), self.postprocessors, self.options, self.env ) mdformat-0.7.22/src/mdformat/renderer/_tree.py000066400000000000000000000007641475015777400213210ustar00rootroot00000000000000from markdown_it.tree import SyntaxTreeNode from mdformat.renderer._context import RenderContext class RenderTreeNode(SyntaxTreeNode): """A syntax tree node capable of making a text rendering of itself.""" def render(self, context: RenderContext) -> str: renderer = context.renderers[self.type] text = renderer(self, context) for postprocessor in context.postprocessors.get(self.type, ()): text = postprocessor(text, self, context) return text mdformat-0.7.22/src/mdformat/renderer/_util.py000066400000000000000000000214221475015777400213310ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable import functools import html.entities import re from typing import TYPE_CHECKING from mdformat import codepoints if TYPE_CHECKING: from mdformat.renderer import RenderTreeNode @functools.cache def re_char_reference() -> re.Pattern[str]: """Return a regex that finds character references. The reference can be either: 1. decimal representation, e.g. 2. hex representation, e.g.  3. HTML5 entity reference, e.g.   This cached function compiles the regex lazily, as compilation can take over 20ms. """ return re.compile( "&(?:" + "#[0-9]{1,7}" + "|" + "#[Xx][0-9A-Fa-f]{1,6}" + "|" + "|".join({c.rstrip(";") for c in html.entities.html5}) + ");" ) def is_tight_list(node: RenderTreeNode) -> bool: assert node.type in {"bullet_list", "ordered_list"} # The list has list items at level +1 so paragraphs in those list # items must be at level +2 (grand children) for child in node.children: for grand_child in child.children: if grand_child.type != "paragraph": continue is_tight = grand_child.hidden if not is_tight: return False return True def is_tight_list_item(node: RenderTreeNode) -> bool: assert node.type == "list_item" assert node.parent is not None return is_tight_list(node.parent) def longest_consecutive_sequence(seq: str, char: str) -> int: """Return length of the longest consecutive sequence of `char` characters in string `seq`.""" assert len(char) == 1 longest = 0 current_streak = 0 for c in seq: if c == char: current_streak += 1 else: current_streak = 0 if current_streak > longest: longest = current_streak return longest def maybe_add_link_brackets(link: str) -> str: """Surround URI with brackets if required by spec.""" if not link or (codepoints.ASCII_CTRL | {" ", "(", ")"}).intersection(link): return "<" + link + ">" return link def get_list_marker_type(node: RenderTreeNode) -> str: if node.type == "bullet_list": mode = "bullet" primary_marker = "-" secondary_marker = "*" else: mode = "ordered" primary_marker = "." secondary_marker = ")" consecutive_lists_count = 1 current = node while True: previous_sibling = current.previous_sibling if previous_sibling is None: return primary_marker if consecutive_lists_count % 2 else secondary_marker prev_type = previous_sibling.type if (mode == "bullet" and prev_type == "bullet_list") or ( mode == "ordered" and prev_type == "ordered_list" ): consecutive_lists_count += 1 current = previous_sibling else: return primary_marker if consecutive_lists_count % 2 else secondary_marker def escape_asterisk_emphasis(text: str) -> str: """Escape asterisks to prevent unexpected emphasis/strong emphasis. Currently we escape all asterisks unless both previous and next character are Unicode whitespace. """ # Fast exit to improve performance if "*" not in text: return text escaped_text = "" text_length = len(text) for i, current_char in enumerate(text): if current_char != "*": escaped_text += current_char continue prev_char = text[i - 1] if (i - 1) >= 0 else None next_char = text[i + 1] if (i + 1) < text_length else None if ( prev_char in codepoints.UNICODE_WHITESPACE and next_char in codepoints.UNICODE_WHITESPACE ): escaped_text += current_char continue escaped_text += "\\" + current_char return escaped_text def escape_underscore_emphasis(text: str) -> str: """Escape underscores to prevent unexpected emphasis/strong emphasis. Currently we escape all underscores unless: - Neither of the surrounding characters are one of Unicode whitespace, start or end of line, or Unicode punctuation - Both surrounding characters are Unicode whitespace """ # Fast exit to improve performance if "_" not in text: return text bad_neighbor_chars = ( codepoints.UNICODE_WHITESPACE | codepoints.UNICODE_PUNCTUATION | frozenset({None}) ) escaped_text = "" text_length = len(text) for i, current_char in enumerate(text): if current_char != "_": escaped_text += current_char continue prev_char = text[i - 1] if (i - 1) >= 0 else None next_char = text[i + 1] if (i + 1) < text_length else None if ( prev_char in codepoints.UNICODE_WHITESPACE and next_char in codepoints.UNICODE_WHITESPACE ) or ( prev_char not in bad_neighbor_chars and next_char not in bad_neighbor_chars ): escaped_text += current_char continue escaped_text += "\\" + current_char return escaped_text def decimalify_leading(char_set: Iterable[str], text: str) -> str: """Replace first character with decimal representation if it's included in `char_set`.""" if not char_set or not text: return text first_char = text[0] if first_char in char_set: return f"&#{ord(first_char)};{text[1:]}" return text def decimalify_trailing(char_set: Iterable[str], text: str) -> str: """Replace last character with decimal representation if it's included in `char_set`.""" if not char_set or not text: return text last_char = text[-1] if last_char in char_set: return f"{text[:-1]}&#{ord(last_char)};" return text def split_at_indexes(text: str, indexes: Iterable[int]) -> list[str]: """Return the text in parts. Make splits right before the indexed character. """ if not indexes: raise ValueError("indexes must not be empty") parts = [] prev_i = 0 for i in sorted(indexes): parts.append(text[prev_i:i]) prev_i = i parts.append(text[i:]) return parts def escape_square_brackets(text: str, used_refs: Iterable[str]) -> str: """Return the input string with square brackets ("[" and "]") escaped in a safe way that avoids unintended link labels or refs after formatting. Heuristic to use: Escape all square brackets unless all the following are true for a closed pair of brackets ([ + text + ]): - the brackets enclose text containing no square brackets - the text is not a used_ref (a link label used in a valid link or image) - the enclosure is not followed by ":" or "(" (I believe that this, rather than requiring the enclosure to be followed by a character other than ":" or "(", should be sufficient, as no inline other than 'text' can start with ":" or "(", and a following text inline never exists as it would be included in the same token. """ escape_before_pos = [] pos = 0 enclosure_start: int | None = None while True: bracket_match = RE_SQUARE_BRACKET.search(text, pos) if not bracket_match: # pragma: >=3.10 cover if enclosure_start is not None: escape_before_pos.append(enclosure_start) break bracket = bracket_match.group() bracket_pos = bracket_match.start() pos = bracket_pos + 1 if bracket == "[": if enclosure_start is not None: escape_before_pos.append(enclosure_start) enclosure_start = bracket_pos else: if enclosure_start is None: escape_before_pos.append(bracket_pos) else: enclosed = text[enclosure_start + 1 : bracket_pos] next_char = text[bracket_pos + 1 : bracket_pos + 2] # can be empty str if enclosed.upper() not in used_refs and next_char not in {":", "("}: enclosure_start = None else: escape_before_pos.append(enclosure_start) escape_before_pos.append(bracket_pos) enclosure_start = None if not escape_before_pos: return text return "\\".join(split_at_indexes(text, escape_before_pos)) RE_SQUARE_BRACKET = re.compile(r"[\[\]]") def escape_less_than_sign(text: str) -> str: """Escape less than sign ('<') to prevent unexpected HTML or autolink. Current heuristic to use: Always escape, except when - followed by a space: This should be safe. Neither HTML nor autolink allow space after the '<' sign """ return RE_LESS_THAN_SIGN__NO_FOLLOWING_SPACE.sub(r"\\\g<0>", text) RE_LESS_THAN_SIGN__NO_FOLLOWING_SPACE = re.compile("<(?:[^ ]|$)") mdformat-0.7.22/src/mdformat/renderer/typing.py000066400000000000000000000007261475015777400215330ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from mdformat.renderer import RenderTreeNode # There should be `mdformat.renderer.RenderContext` instead of `Any` # here and in `Postprocess` below but that results in recursive typing # which mypy doesn't support until # https://github.com/python/mypy/issues/731 is implemented. Render = Callable[ ["RenderTreeNode", Any], str, ] Postprocess = Callable[[str, "RenderTreeNode", Any], str] mdformat-0.7.22/tests/000077500000000000000000000000001475015777400145765ustar00rootroot00000000000000mdformat-0.7.22/tests/__init__.py000066400000000000000000000000001475015777400166750ustar00rootroot00000000000000mdformat-0.7.22/tests/data/000077500000000000000000000000001475015777400155075ustar00rootroot00000000000000mdformat-0.7.22/tests/data/commonmark_spec_v0.30.json000066400000000000000000004222101475015777400224060ustar00rootroot00000000000000[ { "markdown": "\tfoo\tbaz\t\tbim\n", "html": "
foo\tbaz\t\tbim\n
\n", "example": 1, "start_line": 356, "end_line": 361, "section": "Tabs" }, { "markdown": " \tfoo\tbaz\t\tbim\n", "html": "
foo\tbaz\t\tbim\n
\n", "example": 2, "start_line": 363, "end_line": 368, "section": "Tabs" }, { "markdown": " a\ta\n ὐ\ta\n", "html": "
a\ta\nὐ\ta\n
\n", "example": 3, "start_line": 370, "end_line": 377, "section": "Tabs" }, { "markdown": " - foo\n\n\tbar\n", "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", "example": 4, "start_line": 383, "end_line": 394, "section": "Tabs" }, { "markdown": "- foo\n\n\t\tbar\n", "html": "
    \n
  • \n

    foo

    \n
      bar\n
    \n
  • \n
\n", "example": 5, "start_line": 396, "end_line": 408, "section": "Tabs" }, { "markdown": ">\t\tfoo\n", "html": "
\n
  foo\n
\n
\n", "example": 6, "start_line": 419, "end_line": 426, "section": "Tabs" }, { "markdown": "-\t\tfoo\n", "html": "
    \n
  • \n
      foo\n
    \n
  • \n
\n", "example": 7, "start_line": 428, "end_line": 437, "section": "Tabs" }, { "markdown": " foo\n\tbar\n", "html": "
foo\nbar\n
\n", "example": 8, "start_line": 440, "end_line": 447, "section": "Tabs" }, { "markdown": " - foo\n - bar\n\t - baz\n", "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz
      • \n
      \n
    • \n
    \n
  • \n
\n", "example": 9, "start_line": 449, "end_line": 465, "section": "Tabs" }, { "markdown": "#\tFoo\n", "html": "

Foo

\n", "example": 10, "start_line": 467, "end_line": 471, "section": "Tabs" }, { "markdown": "*\t*\t*\t\n", "html": "
\n", "example": 11, "start_line": 473, "end_line": 477, "section": "Tabs" }, { "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n", "html": "

!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~

\n", "example": 12, "start_line": 490, "end_line": 494, "section": "Backslash escapes" }, { "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n", "html": "

\\\t\\A\\a\\ \\3\\φ\\«

\n", "example": 13, "start_line": 500, "end_line": 504, "section": "Backslash escapes" }, { "markdown": "\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\ö not a character entity\n", "html": "

*not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"\n&ouml; not a character entity

\n", "example": 14, "start_line": 510, "end_line": 530, "section": "Backslash escapes" }, { "markdown": "\\\\*emphasis*\n", "html": "

\\emphasis

\n", "example": 15, "start_line": 535, "end_line": 539, "section": "Backslash escapes" }, { "markdown": "foo\\\nbar\n", "html": "

foo
\nbar

\n", "example": 16, "start_line": 544, "end_line": 550, "section": "Backslash escapes" }, { "markdown": "`` \\[\\` ``\n", "html": "

\\[\\`

\n", "example": 17, "start_line": 556, "end_line": 560, "section": "Backslash escapes" }, { "markdown": " \\[\\]\n", "html": "
\\[\\]\n
\n", "example": 18, "start_line": 563, "end_line": 568, "section": "Backslash escapes" }, { "markdown": "~~~\n\\[\\]\n~~~\n", "html": "
\\[\\]\n
\n", "example": 19, "start_line": 571, "end_line": 578, "section": "Backslash escapes" }, { "markdown": "\n", "html": "

http://example.com?find=\\*

\n", "example": 20, "start_line": 581, "end_line": 585, "section": "Backslash escapes" }, { "markdown": "\n", "html": "\n", "example": 21, "start_line": 588, "end_line": 592, "section": "Backslash escapes" }, { "markdown": "[foo](/bar\\* \"ti\\*tle\")\n", "html": "

foo

\n", "example": 22, "start_line": 598, "end_line": 602, "section": "Backslash escapes" }, { "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n", "html": "

foo

\n", "example": 23, "start_line": 605, "end_line": 611, "section": "Backslash escapes" }, { "markdown": "``` foo\\+bar\nfoo\n```\n", "html": "
foo\n
\n", "example": 24, "start_line": 614, "end_line": 621, "section": "Backslash escapes" }, { "markdown": "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n", "html": "

  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸

\n", "example": 25, "start_line": 650, "end_line": 658, "section": "Entity and numeric character references" }, { "markdown": "# Ӓ Ϡ �\n", "html": "

# Ӓ Ϡ �

\n", "example": 26, "start_line": 669, "end_line": 673, "section": "Entity and numeric character references" }, { "markdown": "" ആ ಫ\n", "html": "

" ആ ಫ

\n", "example": 27, "start_line": 682, "end_line": 686, "section": "Entity and numeric character references" }, { "markdown": "  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n", "html": "

&nbsp &x; &#; &#x;\n&#87654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;

\n", "example": 28, "start_line": 691, "end_line": 701, "section": "Entity and numeric character references" }, { "markdown": "©\n", "html": "

&copy

\n", "example": 29, "start_line": 708, "end_line": 712, "section": "Entity and numeric character references" }, { "markdown": "&MadeUpEntity;\n", "html": "

&MadeUpEntity;

\n", "example": 30, "start_line": 718, "end_line": 722, "section": "Entity and numeric character references" }, { "markdown": "\n", "html": "\n", "example": 31, "start_line": 729, "end_line": 733, "section": "Entity and numeric character references" }, { "markdown": "[foo](/föö \"föö\")\n", "html": "

foo

\n", "example": 32, "start_line": 736, "end_line": 740, "section": "Entity and numeric character references" }, { "markdown": "[foo]\n\n[foo]: /föö \"föö\"\n", "html": "

foo

\n", "example": 33, "start_line": 743, "end_line": 749, "section": "Entity and numeric character references" }, { "markdown": "``` föö\nfoo\n```\n", "html": "
foo\n
\n", "example": 34, "start_line": 752, "end_line": 759, "section": "Entity and numeric character references" }, { "markdown": "`föö`\n", "html": "

f&ouml;&ouml;

\n", "example": 35, "start_line": 765, "end_line": 769, "section": "Entity and numeric character references" }, { "markdown": " föfö\n", "html": "
f&ouml;f&ouml;\n
\n", "example": 36, "start_line": 772, "end_line": 777, "section": "Entity and numeric character references" }, { "markdown": "*foo*\n*foo*\n", "html": "

*foo*\nfoo

\n", "example": 37, "start_line": 784, "end_line": 790, "section": "Entity and numeric character references" }, { "markdown": "* foo\n\n* foo\n", "html": "

* foo

\n
    \n
  • foo
  • \n
\n", "example": 38, "start_line": 792, "end_line": 801, "section": "Entity and numeric character references" }, { "markdown": "foo bar\n", "html": "

foo\n\nbar

\n", "example": 39, "start_line": 803, "end_line": 809, "section": "Entity and numeric character references" }, { "markdown": " foo\n", "html": "

\tfoo

\n", "example": 40, "start_line": 811, "end_line": 815, "section": "Entity and numeric character references" }, { "markdown": "[a](url "tit")\n", "html": "

[a](url "tit")

\n", "example": 41, "start_line": 818, "end_line": 822, "section": "Entity and numeric character references" }, { "markdown": "- `one\n- two`\n", "html": "
    \n
  • `one
  • \n
  • two`
  • \n
\n", "example": 42, "start_line": 841, "end_line": 849, "section": "Precedence" }, { "markdown": "***\n---\n___\n", "html": "
\n
\n
\n", "example": 43, "start_line": 880, "end_line": 888, "section": "Thematic breaks" }, { "markdown": "+++\n", "html": "

+++

\n", "example": 44, "start_line": 893, "end_line": 897, "section": "Thematic breaks" }, { "markdown": "===\n", "html": "

===

\n", "example": 45, "start_line": 900, "end_line": 904, "section": "Thematic breaks" }, { "markdown": "--\n**\n__\n", "html": "

--\n**\n__

\n", "example": 46, "start_line": 909, "end_line": 917, "section": "Thematic breaks" }, { "markdown": " ***\n ***\n ***\n", "html": "
\n
\n
\n", "example": 47, "start_line": 922, "end_line": 930, "section": "Thematic breaks" }, { "markdown": " ***\n", "html": "
***\n
\n", "example": 48, "start_line": 935, "end_line": 940, "section": "Thematic breaks" }, { "markdown": "Foo\n ***\n", "html": "

Foo\n***

\n", "example": 49, "start_line": 943, "end_line": 949, "section": "Thematic breaks" }, { "markdown": "_____________________________________\n", "html": "
\n", "example": 50, "start_line": 954, "end_line": 958, "section": "Thematic breaks" }, { "markdown": " - - -\n", "html": "
\n", "example": 51, "start_line": 963, "end_line": 967, "section": "Thematic breaks" }, { "markdown": " ** * ** * ** * **\n", "html": "
\n", "example": 52, "start_line": 970, "end_line": 974, "section": "Thematic breaks" }, { "markdown": "- - - -\n", "html": "
\n", "example": 53, "start_line": 977, "end_line": 981, "section": "Thematic breaks" }, { "markdown": "- - - - \n", "html": "
\n", "example": 54, "start_line": 986, "end_line": 990, "section": "Thematic breaks" }, { "markdown": "_ _ _ _ a\n\na------\n\n---a---\n", "html": "

_ _ _ _ a

\n

a------

\n

---a---

\n", "example": 55, "start_line": 995, "end_line": 1005, "section": "Thematic breaks" }, { "markdown": " *-*\n", "html": "

-

\n", "example": 56, "start_line": 1011, "end_line": 1015, "section": "Thematic breaks" }, { "markdown": "- foo\n***\n- bar\n", "html": "
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
\n", "example": 57, "start_line": 1020, "end_line": 1032, "section": "Thematic breaks" }, { "markdown": "Foo\n***\nbar\n", "html": "

Foo

\n
\n

bar

\n", "example": 58, "start_line": 1037, "end_line": 1045, "section": "Thematic breaks" }, { "markdown": "Foo\n---\nbar\n", "html": "

Foo

\n

bar

\n", "example": 59, "start_line": 1054, "end_line": 1061, "section": "Thematic breaks" }, { "markdown": "* Foo\n* * *\n* Bar\n", "html": "
    \n
  • Foo
  • \n
\n
\n
    \n
  • Bar
  • \n
\n", "example": 60, "start_line": 1067, "end_line": 1079, "section": "Thematic breaks" }, { "markdown": "- Foo\n- * * *\n", "html": "
    \n
  • Foo
  • \n
  • \n
    \n
  • \n
\n", "example": 61, "start_line": 1084, "end_line": 1094, "section": "Thematic breaks" }, { "markdown": "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n", "html": "

foo

\n

foo

\n

foo

\n

foo

\n
foo
\n
foo
\n", "example": 62, "start_line": 1113, "end_line": 1127, "section": "ATX headings" }, { "markdown": "####### foo\n", "html": "

####### foo

\n", "example": 63, "start_line": 1132, "end_line": 1136, "section": "ATX headings" }, { "markdown": "#5 bolt\n\n#hashtag\n", "html": "

#5 bolt

\n

#hashtag

\n", "example": 64, "start_line": 1147, "end_line": 1154, "section": "ATX headings" }, { "markdown": "\\## foo\n", "html": "

## foo

\n", "example": 65, "start_line": 1159, "end_line": 1163, "section": "ATX headings" }, { "markdown": "# foo *bar* \\*baz\\*\n", "html": "

foo bar *baz*

\n", "example": 66, "start_line": 1168, "end_line": 1172, "section": "ATX headings" }, { "markdown": "# foo \n", "html": "

foo

\n", "example": 67, "start_line": 1177, "end_line": 1181, "section": "ATX headings" }, { "markdown": " ### foo\n ## foo\n # foo\n", "html": "

foo

\n

foo

\n

foo

\n", "example": 68, "start_line": 1186, "end_line": 1194, "section": "ATX headings" }, { "markdown": " # foo\n", "html": "
# foo\n
\n", "example": 69, "start_line": 1199, "end_line": 1204, "section": "ATX headings" }, { "markdown": "foo\n # bar\n", "html": "

foo\n# bar

\n", "example": 70, "start_line": 1207, "end_line": 1213, "section": "ATX headings" }, { "markdown": "## foo ##\n ### bar ###\n", "html": "

foo

\n

bar

\n", "example": 71, "start_line": 1218, "end_line": 1224, "section": "ATX headings" }, { "markdown": "# foo ##################################\n##### foo ##\n", "html": "

foo

\n
foo
\n", "example": 72, "start_line": 1229, "end_line": 1235, "section": "ATX headings" }, { "markdown": "### foo ### \n", "html": "

foo

\n", "example": 73, "start_line": 1240, "end_line": 1244, "section": "ATX headings" }, { "markdown": "### foo ### b\n", "html": "

foo ### b

\n", "example": 74, "start_line": 1251, "end_line": 1255, "section": "ATX headings" }, { "markdown": "# foo#\n", "html": "

foo#

\n", "example": 75, "start_line": 1260, "end_line": 1264, "section": "ATX headings" }, { "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n", "html": "

foo ###

\n

foo ###

\n

foo #

\n", "example": 76, "start_line": 1270, "end_line": 1278, "section": "ATX headings" }, { "markdown": "****\n## foo\n****\n", "html": "
\n

foo

\n
\n", "example": 77, "start_line": 1284, "end_line": 1292, "section": "ATX headings" }, { "markdown": "Foo bar\n# baz\nBar foo\n", "html": "

Foo bar

\n

baz

\n

Bar foo

\n", "example": 78, "start_line": 1295, "end_line": 1303, "section": "ATX headings" }, { "markdown": "## \n#\n### ###\n", "html": "

\n

\n

\n", "example": 79, "start_line": 1308, "end_line": 1316, "section": "ATX headings" }, { "markdown": "Foo *bar*\n=========\n\nFoo *bar*\n---------\n", "html": "

Foo bar

\n

Foo bar

\n", "example": 80, "start_line": 1351, "end_line": 1360, "section": "Setext headings" }, { "markdown": "Foo *bar\nbaz*\n====\n", "html": "

Foo bar\nbaz

\n", "example": 81, "start_line": 1365, "end_line": 1372, "section": "Setext headings" }, { "markdown": " Foo *bar\nbaz*\t\n====\n", "html": "

Foo bar\nbaz

\n", "example": 82, "start_line": 1379, "end_line": 1386, "section": "Setext headings" }, { "markdown": "Foo\n-------------------------\n\nFoo\n=\n", "html": "

Foo

\n

Foo

\n", "example": 83, "start_line": 1391, "end_line": 1400, "section": "Setext headings" }, { "markdown": " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n", "html": "

Foo

\n

Foo

\n

Foo

\n", "example": 84, "start_line": 1406, "end_line": 1419, "section": "Setext headings" }, { "markdown": " Foo\n ---\n\n Foo\n---\n", "html": "
Foo\n---\n\nFoo\n
\n
\n", "example": 85, "start_line": 1424, "end_line": 1437, "section": "Setext headings" }, { "markdown": "Foo\n ---- \n", "html": "

Foo

\n", "example": 86, "start_line": 1443, "end_line": 1448, "section": "Setext headings" }, { "markdown": "Foo\n ---\n", "html": "

Foo\n---

\n", "example": 87, "start_line": 1453, "end_line": 1459, "section": "Setext headings" }, { "markdown": "Foo\n= =\n\nFoo\n--- -\n", "html": "

Foo\n= =

\n

Foo

\n
\n", "example": 88, "start_line": 1464, "end_line": 1475, "section": "Setext headings" }, { "markdown": "Foo \n-----\n", "html": "

Foo

\n", "example": 89, "start_line": 1480, "end_line": 1485, "section": "Setext headings" }, { "markdown": "Foo\\\n----\n", "html": "

Foo\\

\n", "example": 90, "start_line": 1490, "end_line": 1495, "section": "Setext headings" }, { "markdown": "`Foo\n----\n`\n\n\n", "html": "

`Foo

\n

`

\n

<a title="a lot

\n

of dashes"/>

\n", "example": 91, "start_line": 1501, "end_line": 1514, "section": "Setext headings" }, { "markdown": "> Foo\n---\n", "html": "
\n

Foo

\n
\n
\n", "example": 92, "start_line": 1520, "end_line": 1528, "section": "Setext headings" }, { "markdown": "> foo\nbar\n===\n", "html": "
\n

foo\nbar\n===

\n
\n", "example": 93, "start_line": 1531, "end_line": 1541, "section": "Setext headings" }, { "markdown": "- Foo\n---\n", "html": "
    \n
  • Foo
  • \n
\n
\n", "example": 94, "start_line": 1544, "end_line": 1552, "section": "Setext headings" }, { "markdown": "Foo\nBar\n---\n", "html": "

Foo\nBar

\n", "example": 95, "start_line": 1559, "end_line": 1566, "section": "Setext headings" }, { "markdown": "---\nFoo\n---\nBar\n---\nBaz\n", "html": "
\n

Foo

\n

Bar

\n

Baz

\n", "example": 96, "start_line": 1572, "end_line": 1584, "section": "Setext headings" }, { "markdown": "\n====\n", "html": "

====

\n", "example": 97, "start_line": 1589, "end_line": 1594, "section": "Setext headings" }, { "markdown": "---\n---\n", "html": "
\n
\n", "example": 98, "start_line": 1601, "end_line": 1607, "section": "Setext headings" }, { "markdown": "- foo\n-----\n", "html": "
    \n
  • foo
  • \n
\n
\n", "example": 99, "start_line": 1610, "end_line": 1618, "section": "Setext headings" }, { "markdown": " foo\n---\n", "html": "
foo\n
\n
\n", "example": 100, "start_line": 1621, "end_line": 1628, "section": "Setext headings" }, { "markdown": "> foo\n-----\n", "html": "
\n

foo

\n
\n
\n", "example": 101, "start_line": 1631, "end_line": 1639, "section": "Setext headings" }, { "markdown": "\\> foo\n------\n", "html": "

> foo

\n", "example": 102, "start_line": 1645, "end_line": 1650, "section": "Setext headings" }, { "markdown": "Foo\n\nbar\n---\nbaz\n", "html": "

Foo

\n

bar

\n

baz

\n", "example": 103, "start_line": 1676, "end_line": 1686, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\n---\n\nbaz\n", "html": "

Foo\nbar

\n
\n

baz

\n", "example": 104, "start_line": 1692, "end_line": 1704, "section": "Setext headings" }, { "markdown": "Foo\nbar\n* * *\nbaz\n", "html": "

Foo\nbar

\n
\n

baz

\n", "example": 105, "start_line": 1710, "end_line": 1720, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\\---\nbaz\n", "html": "

Foo\nbar\n---\nbaz

\n", "example": 106, "start_line": 1725, "end_line": 1735, "section": "Setext headings" }, { "markdown": " a simple\n indented code block\n", "html": "
a simple\n  indented code block\n
\n", "example": 107, "start_line": 1753, "end_line": 1760, "section": "Indented code blocks" }, { "markdown": " - foo\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", "example": 108, "start_line": 1767, "end_line": 1778, "section": "Indented code blocks" }, { "markdown": "1. foo\n\n - bar\n", "html": "
    \n
  1. \n

    foo

    \n
      \n
    • bar
    • \n
    \n
  2. \n
\n", "example": 109, "start_line": 1781, "end_line": 1794, "section": "Indented code blocks" }, { "markdown": "
\n *hi*\n\n - one\n", "html": "
<a/>\n*hi*\n\n- one\n
\n", "example": 110, "start_line": 1801, "end_line": 1812, "section": "Indented code blocks" }, { "markdown": " chunk1\n\n chunk2\n \n \n \n chunk3\n", "html": "
chunk1\n\nchunk2\n\n\n\nchunk3\n
\n", "example": 111, "start_line": 1817, "end_line": 1834, "section": "Indented code blocks" }, { "markdown": " chunk1\n \n chunk2\n", "html": "
chunk1\n  \n  chunk2\n
\n", "example": 112, "start_line": 1840, "end_line": 1849, "section": "Indented code blocks" }, { "markdown": "Foo\n bar\n\n", "html": "

Foo\nbar

\n", "example": 113, "start_line": 1855, "end_line": 1862, "section": "Indented code blocks" }, { "markdown": " foo\nbar\n", "html": "
foo\n
\n

bar

\n", "example": 114, "start_line": 1869, "end_line": 1876, "section": "Indented code blocks" }, { "markdown": "# Heading\n foo\nHeading\n------\n foo\n----\n", "html": "

Heading

\n
foo\n
\n

Heading

\n
foo\n
\n
\n", "example": 115, "start_line": 1882, "end_line": 1897, "section": "Indented code blocks" }, { "markdown": " foo\n bar\n", "html": "
    foo\nbar\n
\n", "example": 116, "start_line": 1902, "end_line": 1909, "section": "Indented code blocks" }, { "markdown": "\n \n foo\n \n\n", "html": "
foo\n
\n", "example": 117, "start_line": 1915, "end_line": 1924, "section": "Indented code blocks" }, { "markdown": " foo \n", "html": "
foo  \n
\n", "example": 118, "start_line": 1929, "end_line": 1934, "section": "Indented code blocks" }, { "markdown": "```\n<\n >\n```\n", "html": "
<\n >\n
\n", "example": 119, "start_line": 1984, "end_line": 1993, "section": "Fenced code blocks" }, { "markdown": "~~~\n<\n >\n~~~\n", "html": "
<\n >\n
\n", "example": 120, "start_line": 1998, "end_line": 2007, "section": "Fenced code blocks" }, { "markdown": "``\nfoo\n``\n", "html": "

foo

\n", "example": 121, "start_line": 2011, "end_line": 2017, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n~~~\n```\n", "html": "
aaa\n~~~\n
\n", "example": 122, "start_line": 2022, "end_line": 2031, "section": "Fenced code blocks" }, { "markdown": "~~~\naaa\n```\n~~~\n", "html": "
aaa\n```\n
\n", "example": 123, "start_line": 2034, "end_line": 2043, "section": "Fenced code blocks" }, { "markdown": "````\naaa\n```\n``````\n", "html": "
aaa\n```\n
\n", "example": 124, "start_line": 2048, "end_line": 2057, "section": "Fenced code blocks" }, { "markdown": "~~~~\naaa\n~~~\n~~~~\n", "html": "
aaa\n~~~\n
\n", "example": 125, "start_line": 2060, "end_line": 2069, "section": "Fenced code blocks" }, { "markdown": "```\n", "html": "
\n", "example": 126, "start_line": 2075, "end_line": 2079, "section": "Fenced code blocks" }, { "markdown": "`````\n\n```\naaa\n", "html": "
\n```\naaa\n
\n", "example": 127, "start_line": 2082, "end_line": 2092, "section": "Fenced code blocks" }, { "markdown": "> ```\n> aaa\n\nbbb\n", "html": "
\n
aaa\n
\n
\n

bbb

\n", "example": 128, "start_line": 2095, "end_line": 2106, "section": "Fenced code blocks" }, { "markdown": "```\n\n \n```\n", "html": "
\n  \n
\n", "example": 129, "start_line": 2111, "end_line": 2120, "section": "Fenced code blocks" }, { "markdown": "```\n```\n", "html": "
\n", "example": 130, "start_line": 2125, "end_line": 2130, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\naaa\n```\n", "html": "
aaa\naaa\n
\n", "example": 131, "start_line": 2137, "end_line": 2146, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n aaa\naaa\n ```\n", "html": "
aaa\naaa\naaa\n
\n", "example": 132, "start_line": 2149, "end_line": 2160, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n aaa\n aaa\n ```\n", "html": "
aaa\n aaa\naaa\n
\n", "example": 133, "start_line": 2163, "end_line": 2174, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n ```\n", "html": "
```\naaa\n```\n
\n", "example": 134, "start_line": 2179, "end_line": 2188, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "
aaa\n
\n", "example": 135, "start_line": 2194, "end_line": 2201, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n ```\n", "html": "
aaa\n
\n", "example": 136, "start_line": 2204, "end_line": 2211, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "
aaa\n    ```\n
\n", "example": 137, "start_line": 2216, "end_line": 2224, "section": "Fenced code blocks" }, { "markdown": "``` ```\naaa\n", "html": "

\naaa

\n", "example": 138, "start_line": 2230, "end_line": 2236, "section": "Fenced code blocks" }, { "markdown": "~~~~~~\naaa\n~~~ ~~\n", "html": "
aaa\n~~~ ~~\n
\n", "example": 139, "start_line": 2239, "end_line": 2247, "section": "Fenced code blocks" }, { "markdown": "foo\n```\nbar\n```\nbaz\n", "html": "

foo

\n
bar\n
\n

baz

\n", "example": 140, "start_line": 2253, "end_line": 2264, "section": "Fenced code blocks" }, { "markdown": "foo\n---\n~~~\nbar\n~~~\n# baz\n", "html": "

foo

\n
bar\n
\n

baz

\n", "example": 141, "start_line": 2270, "end_line": 2282, "section": "Fenced code blocks" }, { "markdown": "```ruby\ndef foo(x)\n return 3\nend\n```\n", "html": "
def foo(x)\n  return 3\nend\n
\n", "example": 142, "start_line": 2292, "end_line": 2303, "section": "Fenced code blocks" }, { "markdown": "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n", "html": "
def foo(x)\n  return 3\nend\n
\n", "example": 143, "start_line": 2306, "end_line": 2317, "section": "Fenced code blocks" }, { "markdown": "````;\n````\n", "html": "
\n", "example": 144, "start_line": 2320, "end_line": 2325, "section": "Fenced code blocks" }, { "markdown": "``` aa ```\nfoo\n", "html": "

aa\nfoo

\n", "example": 145, "start_line": 2330, "end_line": 2336, "section": "Fenced code blocks" }, { "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n", "html": "
foo\n
\n", "example": 146, "start_line": 2341, "end_line": 2348, "section": "Fenced code blocks" }, { "markdown": "```\n``` aaa\n```\n", "html": "
``` aaa\n
\n", "example": 147, "start_line": 2353, "end_line": 2360, "section": "Fenced code blocks" }, { "markdown": "
\n
\n**Hello**,\n\n_world_.\n
\n
\n", "html": "
\n
\n**Hello**,\n

world.\n

\n
\n", "example": 148, "start_line": 2432, "end_line": 2447, "section": "HTML blocks" }, { "markdown": "\n \n \n \n
\n hi\n
\n\nokay.\n", "html": "\n \n \n \n
\n hi\n
\n

okay.

\n", "example": 149, "start_line": 2461, "end_line": 2480, "section": "HTML blocks" }, { "markdown": "
\n*foo*\n", "example": 151, "start_line": 2496, "end_line": 2502, "section": "HTML blocks" }, { "markdown": "
\n\n*Markdown*\n\n
\n", "html": "
\n

Markdown

\n
\n", "example": 152, "start_line": 2507, "end_line": 2517, "section": "HTML blocks" }, { "markdown": "
\n
\n", "html": "
\n
\n", "example": 153, "start_line": 2523, "end_line": 2531, "section": "HTML blocks" }, { "markdown": "
\n
\n", "html": "
\n
\n", "example": 154, "start_line": 2534, "end_line": 2542, "section": "HTML blocks" }, { "markdown": "
\n*foo*\n\n*bar*\n", "html": "
\n*foo*\n

bar

\n", "example": 155, "start_line": 2546, "end_line": 2555, "section": "HTML blocks" }, { "markdown": "
\n", "html": "\n", "example": 159, "start_line": 2595, "end_line": 2599, "section": "HTML blocks" }, { "markdown": "
\nfoo\n
\n", "html": "
\nfoo\n
\n", "example": 160, "start_line": 2602, "end_line": 2610, "section": "HTML blocks" }, { "markdown": "
\n``` c\nint x = 33;\n```\n", "html": "
\n``` c\nint x = 33;\n```\n", "example": 161, "start_line": 2619, "end_line": 2629, "section": "HTML blocks" }, { "markdown": "\n*bar*\n\n", "html": "\n*bar*\n\n", "example": 162, "start_line": 2636, "end_line": 2644, "section": "HTML blocks" }, { "markdown": "\n*bar*\n\n", "html": "\n*bar*\n\n", "example": 163, "start_line": 2649, "end_line": 2657, "section": "HTML blocks" }, { "markdown": "\n*bar*\n\n", "html": "\n*bar*\n\n", "example": 164, "start_line": 2660, "end_line": 2668, "section": "HTML blocks" }, { "markdown": "\n*bar*\n", "html": "\n*bar*\n", "example": 165, "start_line": 2671, "end_line": 2677, "section": "HTML blocks" }, { "markdown": "\n*foo*\n\n", "html": "\n*foo*\n\n", "example": 166, "start_line": 2686, "end_line": 2694, "section": "HTML blocks" }, { "markdown": "\n\n*foo*\n\n\n", "html": "\n

foo

\n
\n", "example": 167, "start_line": 2701, "end_line": 2711, "section": "HTML blocks" }, { "markdown": "*foo*\n", "html": "

foo

\n", "example": 168, "start_line": 2719, "end_line": 2723, "section": "HTML blocks" }, { "markdown": "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\nokay\n", "html": "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\n

okay

\n", "example": 169, "start_line": 2735, "end_line": 2751, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

okay

\n", "example": 170, "start_line": 2756, "end_line": 2770, "section": "HTML blocks" }, { "markdown": "\n", "html": "\n", "example": 171, "start_line": 2775, "end_line": 2791, "section": "HTML blocks" }, { "markdown": "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n", "html": "\nh1 {color:red;}\n\np {color:blue;}\n\n

okay

\n", "example": 172, "start_line": 2795, "end_line": 2811, "section": "HTML blocks" }, { "markdown": "\n\nfoo\n", "html": "\n\nfoo\n", "example": 173, "start_line": 2818, "end_line": 2828, "section": "HTML blocks" }, { "markdown": ">
\n> foo\n\nbar\n", "html": "
\n
\nfoo\n
\n

bar

\n", "example": 174, "start_line": 2831, "end_line": 2842, "section": "HTML blocks" }, { "markdown": "-
\n- foo\n", "html": "
    \n
  • \n
    \n
  • \n
  • foo
  • \n
\n", "example": 175, "start_line": 2845, "end_line": 2855, "section": "HTML blocks" }, { "markdown": "\n*foo*\n", "html": "\n

foo

\n", "example": 176, "start_line": 2860, "end_line": 2866, "section": "HTML blocks" }, { "markdown": "*bar*\n*baz*\n", "html": "*bar*\n

baz

\n", "example": 177, "start_line": 2869, "end_line": 2875, "section": "HTML blocks" }, { "markdown": "1. *bar*\n", "html": "1. *bar*\n", "example": 178, "start_line": 2881, "end_line": 2889, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

okay

\n", "example": 179, "start_line": 2894, "end_line": 2906, "section": "HTML blocks" }, { "markdown": "';\n\n?>\nokay\n", "html": "';\n\n?>\n

okay

\n", "example": 180, "start_line": 2912, "end_line": 2926, "section": "HTML blocks" }, { "markdown": "\n", "html": "\n", "example": 181, "start_line": 2931, "end_line": 2935, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

okay

\n", "example": 182, "start_line": 2940, "end_line": 2968, "section": "HTML blocks" }, { "markdown": " \n\n \n", "html": " \n
<!-- foo -->\n
\n", "example": 183, "start_line": 2974, "end_line": 2982, "section": "HTML blocks" }, { "markdown": "
\n\n
\n", "html": "
\n
<div>\n
\n", "example": 184, "start_line": 2985, "end_line": 2993, "section": "HTML blocks" }, { "markdown": "Foo\n
\nbar\n
\n", "html": "

Foo

\n
\nbar\n
\n", "example": 185, "start_line": 2999, "end_line": 3009, "section": "HTML blocks" }, { "markdown": "
\nbar\n
\n*foo*\n", "html": "
\nbar\n
\n*foo*\n", "example": 186, "start_line": 3016, "end_line": 3026, "section": "HTML blocks" }, { "markdown": "Foo\n\nbaz\n", "html": "

Foo\n\nbaz

\n", "example": 187, "start_line": 3031, "end_line": 3039, "section": "HTML blocks" }, { "markdown": "
\n\n*Emphasized* text.\n\n
\n", "html": "
\n

Emphasized text.

\n
\n", "example": 188, "start_line": 3072, "end_line": 3082, "section": "HTML blocks" }, { "markdown": "
\n*Emphasized* text.\n
\n", "html": "
\n*Emphasized* text.\n
\n", "example": 189, "start_line": 3085, "end_line": 3093, "section": "HTML blocks" }, { "markdown": "\n\n\n\n\n\n\n\n
\nHi\n
\n", "html": "\n\n\n\n
\nHi\n
\n", "example": 190, "start_line": 3107, "end_line": 3127, "section": "HTML blocks" }, { "markdown": "\n\n \n\n \n\n \n\n
\n Hi\n
\n", "html": "\n \n
<td>\n  Hi\n</td>\n
\n \n
\n", "example": 191, "start_line": 3134, "end_line": 3155, "section": "HTML blocks" }, { "markdown": "[foo]: /url \"title\"\n\n[foo]\n", "html": "

foo

\n", "example": 192, "start_line": 3183, "end_line": 3189, "section": "Link reference definitions" }, { "markdown": " [foo]: \n /url \n 'the title' \n\n[foo]\n", "html": "

foo

\n", "example": 193, "start_line": 3192, "end_line": 3200, "section": "Link reference definitions" }, { "markdown": "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n", "html": "

Foo*bar]

\n", "example": 194, "start_line": 3203, "end_line": 3209, "section": "Link reference definitions" }, { "markdown": "[Foo bar]:\n\n'title'\n\n[Foo bar]\n", "html": "

Foo bar

\n", "example": 195, "start_line": 3212, "end_line": 3220, "section": "Link reference definitions" }, { "markdown": "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n", "html": "

foo

\n", "example": 196, "start_line": 3225, "end_line": 3239, "section": "Link reference definitions" }, { "markdown": "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n", "html": "

[foo]: /url 'title

\n

with blank line'

\n

[foo]

\n", "example": 197, "start_line": 3244, "end_line": 3254, "section": "Link reference definitions" }, { "markdown": "[foo]:\n/url\n\n[foo]\n", "html": "

foo

\n", "example": 198, "start_line": 3259, "end_line": 3266, "section": "Link reference definitions" }, { "markdown": "[foo]:\n\n[foo]\n", "html": "

[foo]:

\n

[foo]

\n", "example": 199, "start_line": 3271, "end_line": 3278, "section": "Link reference definitions" }, { "markdown": "[foo]: <>\n\n[foo]\n", "html": "

foo

\n", "example": 200, "start_line": 3283, "end_line": 3289, "section": "Link reference definitions" }, { "markdown": "[foo]: (baz)\n\n[foo]\n", "html": "

[foo]: (baz)

\n

[foo]

\n", "example": 201, "start_line": 3294, "end_line": 3301, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n", "html": "

foo

\n", "example": 202, "start_line": 3307, "end_line": 3313, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: url\n", "html": "

foo

\n", "example": 203, "start_line": 3318, "end_line": 3324, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n", "html": "

foo

\n", "example": 204, "start_line": 3330, "end_line": 3337, "section": "Link reference definitions" }, { "markdown": "[FOO]: /url\n\n[Foo]\n", "html": "

Foo

\n", "example": 205, "start_line": 3343, "end_line": 3349, "section": "Link reference definitions" }, { "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n", "html": "

αγω

\n", "example": 206, "start_line": 3352, "end_line": 3358, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n", "html": "", "example": 207, "start_line": 3367, "end_line": 3370, "section": "Link reference definitions" }, { "markdown": "[\nfoo\n]: /url\nbar\n", "html": "

bar

\n", "example": 208, "start_line": 3375, "end_line": 3382, "section": "Link reference definitions" }, { "markdown": "[foo]: /url \"title\" ok\n", "html": "

[foo]: /url "title" ok

\n", "example": 209, "start_line": 3388, "end_line": 3392, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n\"title\" ok\n", "html": "

"title" ok

\n", "example": 210, "start_line": 3397, "end_line": 3402, "section": "Link reference definitions" }, { "markdown": " [foo]: /url \"title\"\n\n[foo]\n", "html": "
[foo]: /url "title"\n
\n

[foo]

\n", "example": 211, "start_line": 3408, "end_line": 3416, "section": "Link reference definitions" }, { "markdown": "```\n[foo]: /url\n```\n\n[foo]\n", "html": "
[foo]: /url\n
\n

[foo]

\n", "example": 212, "start_line": 3422, "end_line": 3432, "section": "Link reference definitions" }, { "markdown": "Foo\n[bar]: /baz\n\n[bar]\n", "html": "

Foo\n[bar]: /baz

\n

[bar]

\n", "example": 213, "start_line": 3437, "end_line": 3446, "section": "Link reference definitions" }, { "markdown": "# [Foo]\n[foo]: /url\n> bar\n", "html": "

Foo

\n
\n

bar

\n
\n", "example": 214, "start_line": 3452, "end_line": 3461, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\nbar\n===\n[foo]\n", "html": "

bar

\n

foo

\n", "example": 215, "start_line": 3463, "end_line": 3471, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n===\n[foo]\n", "html": "

===\nfoo

\n", "example": 216, "start_line": 3473, "end_line": 3480, "section": "Link reference definitions" }, { "markdown": "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n", "html": "

foo,\nbar,\nbaz

\n", "example": 217, "start_line": 3486, "end_line": 3499, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n> [foo]: /url\n", "html": "

foo

\n
\n
\n", "example": 218, "start_line": 3507, "end_line": 3515, "section": "Link reference definitions" }, { "markdown": "aaa\n\nbbb\n", "html": "

aaa

\n

bbb

\n", "example": 219, "start_line": 3529, "end_line": 3536, "section": "Paragraphs" }, { "markdown": "aaa\nbbb\n\nccc\nddd\n", "html": "

aaa\nbbb

\n

ccc\nddd

\n", "example": 220, "start_line": 3541, "end_line": 3552, "section": "Paragraphs" }, { "markdown": "aaa\n\n\nbbb\n", "html": "

aaa

\n

bbb

\n", "example": 221, "start_line": 3557, "end_line": 3565, "section": "Paragraphs" }, { "markdown": " aaa\n bbb\n", "html": "

aaa\nbbb

\n", "example": 222, "start_line": 3570, "end_line": 3576, "section": "Paragraphs" }, { "markdown": "aaa\n bbb\n ccc\n", "html": "

aaa\nbbb\nccc

\n", "example": 223, "start_line": 3582, "end_line": 3590, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "

aaa\nbbb

\n", "example": 224, "start_line": 3596, "end_line": 3602, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "
aaa\n
\n

bbb

\n", "example": 225, "start_line": 3605, "end_line": 3612, "section": "Paragraphs" }, { "markdown": "aaa \nbbb \n", "html": "

aaa
\nbbb

\n", "example": 226, "start_line": 3619, "end_line": 3625, "section": "Paragraphs" }, { "markdown": " \n\naaa\n \n\n# aaa\n\n \n", "html": "

aaa

\n

aaa

\n", "example": 227, "start_line": 3636, "end_line": 3648, "section": "Blank lines" }, { "markdown": "> # Foo\n> bar\n> baz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", "example": 228, "start_line": 3704, "end_line": 3714, "section": "Block quotes" }, { "markdown": "># Foo\n>bar\n> baz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", "example": 229, "start_line": 3719, "end_line": 3729, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", "example": 230, "start_line": 3734, "end_line": 3744, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "
> # Foo\n> bar\n> baz\n
\n", "example": 231, "start_line": 3749, "end_line": 3758, "section": "Block quotes" }, { "markdown": "> # Foo\n> bar\nbaz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", "example": 232, "start_line": 3764, "end_line": 3774, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n> foo\n", "html": "
\n

bar\nbaz\nfoo

\n
\n", "example": 233, "start_line": 3780, "end_line": 3790, "section": "Block quotes" }, { "markdown": "> foo\n---\n", "html": "
\n

foo

\n
\n
\n", "example": 234, "start_line": 3804, "end_line": 3812, "section": "Block quotes" }, { "markdown": "> - foo\n- bar\n", "html": "
\n
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
\n", "example": 235, "start_line": 3824, "end_line": 3836, "section": "Block quotes" }, { "markdown": "> foo\n bar\n", "html": "
\n
foo\n
\n
\n
bar\n
\n", "example": 236, "start_line": 3842, "end_line": 3852, "section": "Block quotes" }, { "markdown": "> ```\nfoo\n```\n", "html": "
\n
\n
\n

foo

\n
\n", "example": 237, "start_line": 3855, "end_line": 3865, "section": "Block quotes" }, { "markdown": "> foo\n - bar\n", "html": "
\n

foo\n- bar

\n
\n", "example": 238, "start_line": 3871, "end_line": 3879, "section": "Block quotes" }, { "markdown": ">\n", "html": "
\n
\n", "example": 239, "start_line": 3895, "end_line": 3900, "section": "Block quotes" }, { "markdown": ">\n> \n> \n", "html": "
\n
\n", "example": 240, "start_line": 3903, "end_line": 3910, "section": "Block quotes" }, { "markdown": ">\n> foo\n> \n", "html": "
\n

foo

\n
\n", "example": 241, "start_line": 3915, "end_line": 3923, "section": "Block quotes" }, { "markdown": "> foo\n\n> bar\n", "html": "
\n

foo

\n
\n
\n

bar

\n
\n", "example": 242, "start_line": 3928, "end_line": 3939, "section": "Block quotes" }, { "markdown": "> foo\n> bar\n", "html": "
\n

foo\nbar

\n
\n", "example": 243, "start_line": 3950, "end_line": 3958, "section": "Block quotes" }, { "markdown": "> foo\n>\n> bar\n", "html": "
\n

foo

\n

bar

\n
\n", "example": 244, "start_line": 3963, "end_line": 3972, "section": "Block quotes" }, { "markdown": "foo\n> bar\n", "html": "

foo

\n
\n

bar

\n
\n", "example": 245, "start_line": 3977, "end_line": 3985, "section": "Block quotes" }, { "markdown": "> aaa\n***\n> bbb\n", "html": "
\n

aaa

\n
\n
\n
\n

bbb

\n
\n", "example": 246, "start_line": 3991, "end_line": 4003, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n", "html": "
\n

bar\nbaz

\n
\n", "example": 247, "start_line": 4009, "end_line": 4017, "section": "Block quotes" }, { "markdown": "> bar\n\nbaz\n", "html": "
\n

bar

\n
\n

baz

\n", "example": 248, "start_line": 4020, "end_line": 4029, "section": "Block quotes" }, { "markdown": "> bar\n>\nbaz\n", "html": "
\n

bar

\n
\n

baz

\n", "example": 249, "start_line": 4032, "end_line": 4041, "section": "Block quotes" }, { "markdown": "> > > foo\nbar\n", "html": "
\n
\n
\n

foo\nbar

\n
\n
\n
\n", "example": 250, "start_line": 4048, "end_line": 4060, "section": "Block quotes" }, { "markdown": ">>> foo\n> bar\n>>baz\n", "html": "
\n
\n
\n

foo\nbar\nbaz

\n
\n
\n
\n", "example": 251, "start_line": 4063, "end_line": 4077, "section": "Block quotes" }, { "markdown": "> code\n\n> not code\n", "html": "
\n
code\n
\n
\n
\n

not code

\n
\n", "example": 252, "start_line": 4085, "end_line": 4097, "section": "Block quotes" }, { "markdown": "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n", "html": "

A paragraph\nwith two lines.

\n
indented code\n
\n
\n

A block quote.

\n
\n", "example": 253, "start_line": 4139, "end_line": 4154, "section": "List items" }, { "markdown": "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", "example": 254, "start_line": 4161, "end_line": 4180, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "
    \n
  • one
  • \n
\n

two

\n", "example": 255, "start_line": 4194, "end_line": 4203, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
\n", "example": 256, "start_line": 4206, "end_line": 4217, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "
    \n
  • one
  • \n
\n
 two\n
\n", "example": 257, "start_line": 4220, "end_line": 4230, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
\n", "example": 258, "start_line": 4233, "end_line": 4244, "section": "List items" }, { "markdown": " > > 1. one\n>>\n>> two\n", "html": "
\n
\n
    \n
  1. \n

    one

    \n

    two

    \n
  2. \n
\n
\n
\n", "example": 259, "start_line": 4255, "end_line": 4270, "section": "List items" }, { "markdown": ">>- one\n>>\n > > two\n", "html": "
\n
\n
    \n
  • one
  • \n
\n

two

\n
\n
\n", "example": 260, "start_line": 4282, "end_line": 4295, "section": "List items" }, { "markdown": "-one\n\n2.two\n", "html": "

-one

\n

2.two

\n", "example": 261, "start_line": 4301, "end_line": 4308, "section": "List items" }, { "markdown": "- foo\n\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", "example": 262, "start_line": 4314, "end_line": 4326, "section": "List items" }, { "markdown": "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n", "html": "
    \n
  1. \n

    foo

    \n
    bar\n
    \n

    baz

    \n
    \n

    bam

    \n
    \n
  2. \n
\n", "example": 263, "start_line": 4331, "end_line": 4353, "section": "List items" }, { "markdown": "- Foo\n\n bar\n\n\n baz\n", "html": "
    \n
  • \n

    Foo

    \n
    bar\n\n\nbaz\n
    \n
  • \n
\n", "example": 264, "start_line": 4359, "end_line": 4377, "section": "List items" }, { "markdown": "123456789. ok\n", "html": "
    \n
  1. ok
  2. \n
\n", "example": 265, "start_line": 4381, "end_line": 4387, "section": "List items" }, { "markdown": "1234567890. not ok\n", "html": "

1234567890. not ok

\n", "example": 266, "start_line": 4390, "end_line": 4394, "section": "List items" }, { "markdown": "0. ok\n", "html": "
    \n
  1. ok
  2. \n
\n", "example": 267, "start_line": 4399, "end_line": 4405, "section": "List items" }, { "markdown": "003. ok\n", "html": "
    \n
  1. ok
  2. \n
\n", "example": 268, "start_line": 4408, "end_line": 4414, "section": "List items" }, { "markdown": "-1. not ok\n", "html": "

-1. not ok

\n", "example": 269, "start_line": 4419, "end_line": 4423, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n
    bar\n
    \n
  • \n
\n", "example": 270, "start_line": 4442, "end_line": 4454, "section": "List items" }, { "markdown": " 10. foo\n\n bar\n", "html": "
    \n
  1. \n

    foo

    \n
    bar\n
    \n
  2. \n
\n", "example": 271, "start_line": 4459, "end_line": 4471, "section": "List items" }, { "markdown": " indented code\n\nparagraph\n\n more code\n", "html": "
indented code\n
\n

paragraph

\n
more code\n
\n", "example": 272, "start_line": 4478, "end_line": 4490, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "
    \n
  1. \n
    indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
\n", "example": 273, "start_line": 4493, "end_line": 4509, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "
    \n
  1. \n
     indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
\n", "example": 274, "start_line": 4515, "end_line": 4531, "section": "List items" }, { "markdown": " foo\n\nbar\n", "html": "

foo

\n

bar

\n", "example": 275, "start_line": 4542, "end_line": 4549, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
    \n
  • foo
  • \n
\n

bar

\n", "example": 276, "start_line": 4552, "end_line": 4561, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", "example": 277, "start_line": 4569, "end_line": 4580, "section": "List items" }, { "markdown": "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n", "html": "
    \n
  • foo
  • \n
  • \n
    bar\n
    \n
  • \n
  • \n
    baz\n
    \n
  • \n
\n", "example": 278, "start_line": 4596, "end_line": 4617, "section": "List items" }, { "markdown": "- \n foo\n", "html": "
    \n
  • foo
  • \n
\n", "example": 279, "start_line": 4622, "end_line": 4629, "section": "List items" }, { "markdown": "-\n\n foo\n", "html": "
    \n
  • \n
\n

foo

\n", "example": 280, "start_line": 4636, "end_line": 4645, "section": "List items" }, { "markdown": "- foo\n-\n- bar\n", "html": "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n", "example": 281, "start_line": 4650, "end_line": 4660, "section": "List items" }, { "markdown": "- foo\n- \n- bar\n", "html": "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n", "example": 282, "start_line": 4665, "end_line": 4675, "section": "List items" }, { "markdown": "1. foo\n2.\n3. bar\n", "html": "
    \n
  1. foo
  2. \n
  3. \n
  4. bar
  5. \n
\n", "example": 283, "start_line": 4680, "end_line": 4690, "section": "List items" }, { "markdown": "*\n", "html": "
    \n
  • \n
\n", "example": 284, "start_line": 4695, "end_line": 4701, "section": "List items" }, { "markdown": "foo\n*\n\nfoo\n1.\n", "html": "

foo\n*

\n

foo\n1.

\n", "example": 285, "start_line": 4705, "end_line": 4716, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", "example": 286, "start_line": 4727, "end_line": 4746, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", "example": 287, "start_line": 4751, "end_line": 4770, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", "example": 288, "start_line": 4775, "end_line": 4794, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
1.  A paragraph\n    with two lines.\n\n        indented code\n\n    > A block quote.\n
\n", "example": 289, "start_line": 4799, "end_line": 4814, "section": "List items" }, { "markdown": " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", "example": 290, "start_line": 4829, "end_line": 4848, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n", "html": "
    \n
  1. A paragraph\nwith two lines.
  2. \n
\n", "example": 291, "start_line": 4853, "end_line": 4861, "section": "List items" }, { "markdown": "> 1. > Blockquote\ncontinued here.\n", "html": "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
\n", "example": 292, "start_line": 4866, "end_line": 4880, "section": "List items" }, { "markdown": "> 1. > Blockquote\n> continued here.\n", "html": "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
\n", "example": 293, "start_line": 4883, "end_line": 4897, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz\n
          \n
        • boo
        • \n
        \n
      • \n
      \n
    • \n
    \n
  • \n
\n", "example": 294, "start_line": 4911, "end_line": 4932, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "
    \n
  • foo
  • \n
  • bar
  • \n
  • baz
  • \n
  • boo
  • \n
\n", "example": 295, "start_line": 4937, "end_line": 4949, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "
    \n
  1. foo\n
      \n
    • bar
    • \n
    \n
  2. \n
\n", "example": 296, "start_line": 4954, "end_line": 4965, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "
    \n
  1. foo
  2. \n
\n
    \n
  • bar
  • \n
\n", "example": 297, "start_line": 4970, "end_line": 4980, "section": "List items" }, { "markdown": "- - foo\n", "html": "
    \n
  • \n
      \n
    • foo
    • \n
    \n
  • \n
\n", "example": 298, "start_line": 4985, "end_line": 4995, "section": "List items" }, { "markdown": "1. - 2. foo\n", "html": "
    \n
  1. \n
      \n
    • \n
        \n
      1. foo
      2. \n
      \n
    • \n
    \n
  2. \n
\n", "example": 299, "start_line": 4998, "end_line": 5012, "section": "List items" }, { "markdown": "- # Foo\n- Bar\n ---\n baz\n", "html": "
    \n
  • \n

    Foo

    \n
  • \n
  • \n

    Bar

    \nbaz
  • \n
\n", "example": 300, "start_line": 5017, "end_line": 5031, "section": "List items" }, { "markdown": "- foo\n- bar\n+ baz\n", "html": "
    \n
  • foo
  • \n
  • bar
  • \n
\n
    \n
  • baz
  • \n
\n", "example": 301, "start_line": 5253, "end_line": 5265, "section": "Lists" }, { "markdown": "1. foo\n2. bar\n3) baz\n", "html": "
    \n
  1. foo
  2. \n
  3. bar
  4. \n
\n
    \n
  1. baz
  2. \n
\n", "example": 302, "start_line": 5268, "end_line": 5280, "section": "Lists" }, { "markdown": "Foo\n- bar\n- baz\n", "html": "

Foo

\n
    \n
  • bar
  • \n
  • baz
  • \n
\n", "example": 303, "start_line": 5287, "end_line": 5297, "section": "Lists" }, { "markdown": "The number of windows in my house is\n14. The number of doors is 6.\n", "html": "

The number of windows in my house is\n14. The number of doors is 6.

\n", "example": 304, "start_line": 5364, "end_line": 5370, "section": "Lists" }, { "markdown": "The number of windows in my house is\n1. The number of doors is 6.\n", "html": "

The number of windows in my house is

\n
    \n
  1. The number of doors is 6.
  2. \n
\n", "example": 305, "start_line": 5374, "end_line": 5382, "section": "Lists" }, { "markdown": "- foo\n\n- bar\n\n\n- baz\n", "html": "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
  • \n

    baz

    \n
  • \n
\n", "example": 306, "start_line": 5388, "end_line": 5407, "section": "Lists" }, { "markdown": "- foo\n - bar\n - baz\n\n\n bim\n", "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • \n

        baz

        \n

        bim

        \n
      • \n
      \n
    • \n
    \n
  • \n
\n", "example": 307, "start_line": 5409, "end_line": 5431, "section": "Lists" }, { "markdown": "- foo\n- bar\n\n\n\n- baz\n- bim\n", "html": "
    \n
  • foo
  • \n
  • bar
  • \n
\n\n
    \n
  • baz
  • \n
  • bim
  • \n
\n", "example": 308, "start_line": 5439, "end_line": 5457, "section": "Lists" }, { "markdown": "- foo\n\n notcode\n\n- foo\n\n\n\n code\n", "html": "
    \n
  • \n

    foo

    \n

    notcode

    \n
  • \n
  • \n

    foo

    \n
  • \n
\n\n
code\n
\n", "example": 309, "start_line": 5460, "end_line": 5483, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n - f\n- g\n", "html": "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d
  • \n
  • e
  • \n
  • f
  • \n
  • g
  • \n
\n", "example": 310, "start_line": 5491, "end_line": 5509, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
  5. \n

    c

    \n
  6. \n
\n", "example": 311, "start_line": 5512, "end_line": 5530, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n", "html": "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d\n- e
  • \n
\n", "example": 312, "start_line": 5536, "end_line": 5550, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
\n
3. c\n
\n", "example": 313, "start_line": 5556, "end_line": 5573, "section": "Lists" }, { "markdown": "- a\n- b\n\n- c\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    c

    \n
  • \n
\n", "example": 314, "start_line": 5579, "end_line": 5596, "section": "Lists" }, { "markdown": "* a\n*\n\n* c\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n
  • \n

    c

    \n
  • \n
\n", "example": 315, "start_line": 5601, "end_line": 5616, "section": "Lists" }, { "markdown": "- a\n- b\n\n c\n- d\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n

    c

    \n
  • \n
  • \n

    d

    \n
  • \n
\n", "example": 316, "start_line": 5623, "end_line": 5642, "section": "Lists" }, { "markdown": "- a\n- b\n\n [ref]: /url\n- d\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    d

    \n
  • \n
\n", "example": 317, "start_line": 5645, "end_line": 5663, "section": "Lists" }, { "markdown": "- a\n- ```\n b\n\n\n ```\n- c\n", "html": "
    \n
  • a
  • \n
  • \n
    b\n\n\n
    \n
  • \n
  • c
  • \n
\n", "example": 318, "start_line": 5668, "end_line": 5687, "section": "Lists" }, { "markdown": "- a\n - b\n\n c\n- d\n", "html": "
    \n
  • a\n
      \n
    • \n

      b

      \n

      c

      \n
    • \n
    \n
  • \n
  • d
  • \n
\n", "example": 319, "start_line": 5694, "end_line": 5712, "section": "Lists" }, { "markdown": "* a\n > b\n >\n* c\n", "html": "
    \n
  • a\n
    \n

    b

    \n
    \n
  • \n
  • c
  • \n
\n", "example": 320, "start_line": 5718, "end_line": 5732, "section": "Lists" }, { "markdown": "- a\n > b\n ```\n c\n ```\n- d\n", "html": "
    \n
  • a\n
    \n

    b

    \n
    \n
    c\n
    \n
  • \n
  • d
  • \n
\n", "example": 321, "start_line": 5738, "end_line": 5756, "section": "Lists" }, { "markdown": "- a\n", "html": "
    \n
  • a
  • \n
\n", "example": 322, "start_line": 5761, "end_line": 5767, "section": "Lists" }, { "markdown": "- a\n - b\n", "html": "
    \n
  • a\n
      \n
    • b
    • \n
    \n
  • \n
\n", "example": 323, "start_line": 5770, "end_line": 5781, "section": "Lists" }, { "markdown": "1. ```\n foo\n ```\n\n bar\n", "html": "
    \n
  1. \n
    foo\n
    \n

    bar

    \n
  2. \n
\n", "example": 324, "start_line": 5787, "end_line": 5801, "section": "Lists" }, { "markdown": "* foo\n * bar\n\n baz\n", "html": "
    \n
  • \n

    foo

    \n
      \n
    • bar
    • \n
    \n

    baz

    \n
  • \n
\n", "example": 325, "start_line": 5806, "end_line": 5821, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n\n- d\n - e\n - f\n", "html": "
    \n
  • \n

    a

    \n
      \n
    • b
    • \n
    • c
    • \n
    \n
  • \n
  • \n

    d

    \n
      \n
    • e
    • \n
    • f
    • \n
    \n
  • \n
\n", "example": 326, "start_line": 5824, "end_line": 5849, "section": "Lists" }, { "markdown": "`hi`lo`\n", "html": "

hilo`

\n", "example": 327, "start_line": 5858, "end_line": 5862, "section": "Inlines" }, { "markdown": "`foo`\n", "html": "

foo

\n", "example": 328, "start_line": 5890, "end_line": 5894, "section": "Code spans" }, { "markdown": "`` foo ` bar ``\n", "html": "

foo ` bar

\n", "example": 329, "start_line": 5901, "end_line": 5905, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "

``

\n", "example": 330, "start_line": 5911, "end_line": 5915, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "

``

\n", "example": 331, "start_line": 5919, "end_line": 5923, "section": "Code spans" }, { "markdown": "` a`\n", "html": "

a

\n", "example": 332, "start_line": 5928, "end_line": 5932, "section": "Code spans" }, { "markdown": "` b `\n", "html": "

 b 

\n", "example": 333, "start_line": 5937, "end_line": 5941, "section": "Code spans" }, { "markdown": "` `\n` `\n", "html": "

 \n

\n", "example": 334, "start_line": 5945, "end_line": 5951, "section": "Code spans" }, { "markdown": "``\nfoo\nbar \nbaz\n``\n", "html": "

foo bar baz

\n", "example": 335, "start_line": 5956, "end_line": 5964, "section": "Code spans" }, { "markdown": "``\nfoo \n``\n", "html": "

foo

\n", "example": 336, "start_line": 5966, "end_line": 5972, "section": "Code spans" }, { "markdown": "`foo bar \nbaz`\n", "html": "

foo bar baz

\n", "example": 337, "start_line": 5977, "end_line": 5982, "section": "Code spans" }, { "markdown": "`foo\\`bar`\n", "html": "

foo\\bar`

\n", "example": 338, "start_line": 5994, "end_line": 5998, "section": "Code spans" }, { "markdown": "``foo`bar``\n", "html": "

foo`bar

\n", "example": 339, "start_line": 6005, "end_line": 6009, "section": "Code spans" }, { "markdown": "` foo `` bar `\n", "html": "

foo `` bar

\n", "example": 340, "start_line": 6011, "end_line": 6015, "section": "Code spans" }, { "markdown": "*foo`*`\n", "html": "

*foo*

\n", "example": 341, "start_line": 6023, "end_line": 6027, "section": "Code spans" }, { "markdown": "[not a `link](/foo`)\n", "html": "

[not a link](/foo)

\n", "example": 342, "start_line": 6032, "end_line": 6036, "section": "Code spans" }, { "markdown": "``\n", "html": "

<a href="">`

\n", "example": 343, "start_line": 6042, "end_line": 6046, "section": "Code spans" }, { "markdown": "
`\n", "html": "

`

\n", "example": 344, "start_line": 6051, "end_line": 6055, "section": "Code spans" }, { "markdown": "``\n", "html": "

<http://foo.bar.baz>`

\n", "example": 345, "start_line": 6060, "end_line": 6064, "section": "Code spans" }, { "markdown": "`\n", "html": "

http://foo.bar.`baz`

\n", "example": 346, "start_line": 6069, "end_line": 6073, "section": "Code spans" }, { "markdown": "```foo``\n", "html": "

```foo``

\n", "example": 347, "start_line": 6079, "end_line": 6083, "section": "Code spans" }, { "markdown": "`foo\n", "html": "

`foo

\n", "example": 348, "start_line": 6086, "end_line": 6090, "section": "Code spans" }, { "markdown": "`foo``bar``\n", "html": "

`foobar

\n", "example": 349, "start_line": 6095, "end_line": 6099, "section": "Code spans" }, { "markdown": "*foo bar*\n", "html": "

foo bar

\n", "example": 350, "start_line": 6312, "end_line": 6316, "section": "Emphasis and strong emphasis" }, { "markdown": "a * foo bar*\n", "html": "

a * foo bar*

\n", "example": 351, "start_line": 6322, "end_line": 6326, "section": "Emphasis and strong emphasis" }, { "markdown": "a*\"foo\"*\n", "html": "

a*"foo"*

\n", "example": 352, "start_line": 6333, "end_line": 6337, "section": "Emphasis and strong emphasis" }, { "markdown": "* a *\n", "html": "

* a *

\n", "example": 353, "start_line": 6342, "end_line": 6346, "section": "Emphasis and strong emphasis" }, { "markdown": "foo*bar*\n", "html": "

foobar

\n", "example": 354, "start_line": 6351, "end_line": 6355, "section": "Emphasis and strong emphasis" }, { "markdown": "5*6*78\n", "html": "

5678

\n", "example": 355, "start_line": 6358, "end_line": 6362, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar_\n", "html": "

foo bar

\n", "example": 356, "start_line": 6367, "end_line": 6371, "section": "Emphasis and strong emphasis" }, { "markdown": "_ foo bar_\n", "html": "

_ foo bar_

\n", "example": 357, "start_line": 6377, "end_line": 6381, "section": "Emphasis and strong emphasis" }, { "markdown": "a_\"foo\"_\n", "html": "

a_"foo"_

\n", "example": 358, "start_line": 6387, "end_line": 6391, "section": "Emphasis and strong emphasis" }, { "markdown": "foo_bar_\n", "html": "

foo_bar_

\n", "example": 359, "start_line": 6396, "end_line": 6400, "section": "Emphasis and strong emphasis" }, { "markdown": "5_6_78\n", "html": "

5_6_78

\n", "example": 360, "start_line": 6403, "end_line": 6407, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням_стремятся_\n", "html": "

пристаням_стремятся_

\n", "example": 361, "start_line": 6410, "end_line": 6414, "section": "Emphasis and strong emphasis" }, { "markdown": "aa_\"bb\"_cc\n", "html": "

aa_"bb"_cc

\n", "example": 362, "start_line": 6420, "end_line": 6424, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-_(bar)_\n", "html": "

foo-(bar)

\n", "example": 363, "start_line": 6431, "end_line": 6435, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo*\n", "html": "

_foo*

\n", "example": 364, "start_line": 6443, "end_line": 6447, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar *\n", "html": "

*foo bar *

\n", "example": 365, "start_line": 6453, "end_line": 6457, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar\n*\n", "html": "

*foo bar\n*

\n", "example": 366, "start_line": 6462, "end_line": 6468, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo)\n", "html": "

*(*foo)

\n", "example": 367, "start_line": 6475, "end_line": 6479, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo*)*\n", "html": "

(foo)

\n", "example": 368, "start_line": 6485, "end_line": 6489, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo*bar\n", "html": "

foobar

\n", "example": 369, "start_line": 6494, "end_line": 6498, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar _\n", "html": "

_foo bar _

\n", "example": 370, "start_line": 6507, "end_line": 6511, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo)\n", "html": "

_(_foo)

\n", "example": 371, "start_line": 6517, "end_line": 6521, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo_)_\n", "html": "

(foo)

\n", "example": 372, "start_line": 6526, "end_line": 6530, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar\n", "html": "

_foo_bar

\n", "example": 373, "start_line": 6535, "end_line": 6539, "section": "Emphasis and strong emphasis" }, { "markdown": "_пристаням_стремятся\n", "html": "

_пристаням_стремятся

\n", "example": 374, "start_line": 6542, "end_line": 6546, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar_baz_\n", "html": "

foo_bar_baz

\n", "example": 375, "start_line": 6549, "end_line": 6553, "section": "Emphasis and strong emphasis" }, { "markdown": "_(bar)_.\n", "html": "

(bar).

\n", "example": 376, "start_line": 6560, "end_line": 6564, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar**\n", "html": "

foo bar

\n", "example": 377, "start_line": 6569, "end_line": 6573, "section": "Emphasis and strong emphasis" }, { "markdown": "** foo bar**\n", "html": "

** foo bar**

\n", "example": 378, "start_line": 6579, "end_line": 6583, "section": "Emphasis and strong emphasis" }, { "markdown": "a**\"foo\"**\n", "html": "

a**"foo"**

\n", "example": 379, "start_line": 6590, "end_line": 6594, "section": "Emphasis and strong emphasis" }, { "markdown": "foo**bar**\n", "html": "

foobar

\n", "example": 380, "start_line": 6599, "end_line": 6603, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar__\n", "html": "

foo bar

\n", "example": 381, "start_line": 6608, "end_line": 6612, "section": "Emphasis and strong emphasis" }, { "markdown": "__ foo bar__\n", "html": "

__ foo bar__

\n", "example": 382, "start_line": 6618, "end_line": 6622, "section": "Emphasis and strong emphasis" }, { "markdown": "__\nfoo bar__\n", "html": "

__\nfoo bar__

\n", "example": 383, "start_line": 6626, "end_line": 6632, "section": "Emphasis and strong emphasis" }, { "markdown": "a__\"foo\"__\n", "html": "

a__"foo"__

\n", "example": 384, "start_line": 6638, "end_line": 6642, "section": "Emphasis and strong emphasis" }, { "markdown": "foo__bar__\n", "html": "

foo__bar__

\n", "example": 385, "start_line": 6647, "end_line": 6651, "section": "Emphasis and strong emphasis" }, { "markdown": "5__6__78\n", "html": "

5__6__78

\n", "example": 386, "start_line": 6654, "end_line": 6658, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням__стремятся__\n", "html": "

пристаням__стремятся__

\n", "example": 387, "start_line": 6661, "end_line": 6665, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo, __bar__, baz__\n", "html": "

foo, bar, baz

\n", "example": 388, "start_line": 6668, "end_line": 6672, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-__(bar)__\n", "html": "

foo-(bar)

\n", "example": 389, "start_line": 6679, "end_line": 6683, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar **\n", "html": "

**foo bar **

\n", "example": 390, "start_line": 6692, "end_line": 6696, "section": "Emphasis and strong emphasis" }, { "markdown": "**(**foo)\n", "html": "

**(**foo)

\n", "example": 391, "start_line": 6705, "end_line": 6709, "section": "Emphasis and strong emphasis" }, { "markdown": "*(**foo**)*\n", "html": "

(foo)

\n", "example": 392, "start_line": 6715, "end_line": 6719, "section": "Emphasis and strong emphasis" }, { "markdown": "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n", "html": "

Gomphocarpus (Gomphocarpus physocarpus, syn.\nAsclepias physocarpa)

\n", "example": 393, "start_line": 6722, "end_line": 6728, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo \"*bar*\" foo**\n", "html": "

foo "bar" foo

\n", "example": 394, "start_line": 6731, "end_line": 6735, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**bar\n", "html": "

foobar

\n", "example": 395, "start_line": 6740, "end_line": 6744, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar __\n", "html": "

__foo bar __

\n", "example": 396, "start_line": 6752, "end_line": 6756, "section": "Emphasis and strong emphasis" }, { "markdown": "__(__foo)\n", "html": "

__(__foo)

\n", "example": 397, "start_line": 6762, "end_line": 6766, "section": "Emphasis and strong emphasis" }, { "markdown": "_(__foo__)_\n", "html": "

(foo)

\n", "example": 398, "start_line": 6772, "end_line": 6776, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar\n", "html": "

__foo__bar

\n", "example": 399, "start_line": 6781, "end_line": 6785, "section": "Emphasis and strong emphasis" }, { "markdown": "__пристаням__стремятся\n", "html": "

__пристаням__стремятся

\n", "example": 400, "start_line": 6788, "end_line": 6792, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar__baz__\n", "html": "

foo__bar__baz

\n", "example": 401, "start_line": 6795, "end_line": 6799, "section": "Emphasis and strong emphasis" }, { "markdown": "__(bar)__.\n", "html": "

(bar).

\n", "example": 402, "start_line": 6806, "end_line": 6810, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [bar](/url)*\n", "html": "

foo bar

\n", "example": 403, "start_line": 6818, "end_line": 6822, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo\nbar*\n", "html": "

foo\nbar

\n", "example": 404, "start_line": 6825, "end_line": 6831, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo __bar__ baz_\n", "html": "

foo bar baz

\n", "example": 405, "start_line": 6837, "end_line": 6841, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo _bar_ baz_\n", "html": "

foo bar baz

\n", "example": 406, "start_line": 6844, "end_line": 6848, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_ bar_\n", "html": "

foo bar

\n", "example": 407, "start_line": 6851, "end_line": 6855, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar**\n", "html": "

foo bar

\n", "example": 408, "start_line": 6858, "end_line": 6862, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar** baz*\n", "html": "

foo bar baz

\n", "example": 409, "start_line": 6865, "end_line": 6869, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar**baz*\n", "html": "

foobarbaz

\n", "example": 410, "start_line": 6871, "end_line": 6875, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar*\n", "html": "

foo**bar

\n", "example": 411, "start_line": 6895, "end_line": 6899, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo** bar*\n", "html": "

foo bar

\n", "example": 412, "start_line": 6908, "end_line": 6912, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar***\n", "html": "

foo bar

\n", "example": 413, "start_line": 6915, "end_line": 6919, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar***\n", "html": "

foobar

\n", "example": 414, "start_line": 6922, "end_line": 6926, "section": "Emphasis and strong emphasis" }, { "markdown": "foo***bar***baz\n", "html": "

foobarbaz

\n", "example": 415, "start_line": 6933, "end_line": 6937, "section": "Emphasis and strong emphasis" }, { "markdown": "foo******bar*********baz\n", "html": "

foobar***baz

\n", "example": 416, "start_line": 6939, "end_line": 6943, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar *baz* bim** bop*\n", "html": "

foo bar baz bim bop

\n", "example": 417, "start_line": 6948, "end_line": 6952, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [*bar*](/url)*\n", "html": "

foo bar

\n", "example": 418, "start_line": 6955, "end_line": 6959, "section": "Emphasis and strong emphasis" }, { "markdown": "** is not an empty emphasis\n", "html": "

** is not an empty emphasis

\n", "example": 419, "start_line": 6964, "end_line": 6968, "section": "Emphasis and strong emphasis" }, { "markdown": "**** is not an empty strong emphasis\n", "html": "

**** is not an empty strong emphasis

\n", "example": 420, "start_line": 6971, "end_line": 6975, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [bar](/url)**\n", "html": "

foo bar

\n", "example": 421, "start_line": 6984, "end_line": 6988, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo\nbar**\n", "html": "

foo\nbar

\n", "example": 422, "start_line": 6991, "end_line": 6997, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo _bar_ baz__\n", "html": "

foo bar baz

\n", "example": 423, "start_line": 7003, "end_line": 7007, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo __bar__ baz__\n", "html": "

foo bar baz

\n", "example": 424, "start_line": 7010, "end_line": 7014, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo__ bar__\n", "html": "

foo bar

\n", "example": 425, "start_line": 7017, "end_line": 7021, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar****\n", "html": "

foo bar

\n", "example": 426, "start_line": 7024, "end_line": 7028, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar* baz**\n", "html": "

foo bar baz

\n", "example": 427, "start_line": 7031, "end_line": 7035, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*bar*baz**\n", "html": "

foobarbaz

\n", "example": 428, "start_line": 7038, "end_line": 7042, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo* bar**\n", "html": "

foo bar

\n", "example": 429, "start_line": 7045, "end_line": 7049, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar***\n", "html": "

foo bar

\n", "example": 430, "start_line": 7052, "end_line": 7056, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar **baz**\nbim* bop**\n", "html": "

foo bar baz\nbim bop

\n", "example": 431, "start_line": 7061, "end_line": 7067, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [*bar*](/url)**\n", "html": "

foo bar

\n", "example": 432, "start_line": 7070, "end_line": 7074, "section": "Emphasis and strong emphasis" }, { "markdown": "__ is not an empty emphasis\n", "html": "

__ is not an empty emphasis

\n", "example": 433, "start_line": 7079, "end_line": 7083, "section": "Emphasis and strong emphasis" }, { "markdown": "____ is not an empty strong emphasis\n", "html": "

____ is not an empty strong emphasis

\n", "example": 434, "start_line": 7086, "end_line": 7090, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ***\n", "html": "

foo ***

\n", "example": 435, "start_line": 7096, "end_line": 7100, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *\\**\n", "html": "

foo *

\n", "example": 436, "start_line": 7103, "end_line": 7107, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *_*\n", "html": "

foo _

\n", "example": 437, "start_line": 7110, "end_line": 7114, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *****\n", "html": "

foo *****

\n", "example": 438, "start_line": 7117, "end_line": 7121, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **\\***\n", "html": "

foo *

\n", "example": 439, "start_line": 7124, "end_line": 7128, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **_**\n", "html": "

foo _

\n", "example": 440, "start_line": 7131, "end_line": 7135, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*\n", "html": "

*foo

\n", "example": 441, "start_line": 7142, "end_line": 7146, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**\n", "html": "

foo*

\n", "example": 442, "start_line": 7149, "end_line": 7153, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo**\n", "html": "

*foo

\n", "example": 443, "start_line": 7156, "end_line": 7160, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo*\n", "html": "

***foo

\n", "example": 444, "start_line": 7163, "end_line": 7167, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo***\n", "html": "

foo*

\n", "example": 445, "start_line": 7170, "end_line": 7174, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo****\n", "html": "

foo***

\n", "example": 446, "start_line": 7177, "end_line": 7181, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ___\n", "html": "

foo ___

\n", "example": 447, "start_line": 7187, "end_line": 7191, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _\\__\n", "html": "

foo _

\n", "example": 448, "start_line": 7194, "end_line": 7198, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _*_\n", "html": "

foo *

\n", "example": 449, "start_line": 7201, "end_line": 7205, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _____\n", "html": "

foo _____

\n", "example": 450, "start_line": 7208, "end_line": 7212, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __\\___\n", "html": "

foo _

\n", "example": 451, "start_line": 7215, "end_line": 7219, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __*__\n", "html": "

foo *

\n", "example": 452, "start_line": 7222, "end_line": 7226, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_\n", "html": "

_foo

\n", "example": 453, "start_line": 7229, "end_line": 7233, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo__\n", "html": "

foo_

\n", "example": 454, "start_line": 7240, "end_line": 7244, "section": "Emphasis and strong emphasis" }, { "markdown": "___foo__\n", "html": "

_foo

\n", "example": 455, "start_line": 7247, "end_line": 7251, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo_\n", "html": "

___foo

\n", "example": 456, "start_line": 7254, "end_line": 7258, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo___\n", "html": "

foo_

\n", "example": 457, "start_line": 7261, "end_line": 7265, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo____\n", "html": "

foo___

\n", "example": 458, "start_line": 7268, "end_line": 7272, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**\n", "html": "

foo

\n", "example": 459, "start_line": 7278, "end_line": 7282, "section": "Emphasis and strong emphasis" }, { "markdown": "*_foo_*\n", "html": "

foo

\n", "example": 460, "start_line": 7285, "end_line": 7289, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__\n", "html": "

foo

\n", "example": 461, "start_line": 7292, "end_line": 7296, "section": "Emphasis and strong emphasis" }, { "markdown": "_*foo*_\n", "html": "

foo

\n", "example": 462, "start_line": 7299, "end_line": 7303, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo****\n", "html": "

foo

\n", "example": 463, "start_line": 7309, "end_line": 7313, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo____\n", "html": "

foo

\n", "example": 464, "start_line": 7316, "end_line": 7320, "section": "Emphasis and strong emphasis" }, { "markdown": "******foo******\n", "html": "

foo

\n", "example": 465, "start_line": 7327, "end_line": 7331, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo***\n", "html": "

foo

\n", "example": 466, "start_line": 7336, "end_line": 7340, "section": "Emphasis and strong emphasis" }, { "markdown": "_____foo_____\n", "html": "

foo

\n", "example": 467, "start_line": 7343, "end_line": 7347, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo _bar* baz_\n", "html": "

foo _bar baz_

\n", "example": 468, "start_line": 7352, "end_line": 7356, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo __bar *baz bim__ bam*\n", "html": "

foo bar *baz bim bam

\n", "example": 469, "start_line": 7359, "end_line": 7363, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar baz**\n", "html": "

**foo bar baz

\n", "example": 470, "start_line": 7368, "end_line": 7372, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar baz*\n", "html": "

*foo bar baz

\n", "example": 471, "start_line": 7375, "end_line": 7379, "section": "Emphasis and strong emphasis" }, { "markdown": "*[bar*](/url)\n", "html": "

*bar*

\n", "example": 472, "start_line": 7384, "end_line": 7388, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo [bar_](/url)\n", "html": "

_foo bar_

\n", "example": 473, "start_line": 7391, "end_line": 7395, "section": "Emphasis and strong emphasis" }, { "markdown": "*\n", "html": "

*

\n", "example": 474, "start_line": 7398, "end_line": 7402, "section": "Emphasis and strong emphasis" }, { "markdown": "**\n", "html": "

**

\n", "example": 475, "start_line": 7405, "end_line": 7409, "section": "Emphasis and strong emphasis" }, { "markdown": "__\n", "html": "

__

\n", "example": 476, "start_line": 7412, "end_line": 7416, "section": "Emphasis and strong emphasis" }, { "markdown": "*a `*`*\n", "html": "

a *

\n", "example": 477, "start_line": 7419, "end_line": 7423, "section": "Emphasis and strong emphasis" }, { "markdown": "_a `_`_\n", "html": "

a _

\n", "example": 478, "start_line": 7426, "end_line": 7430, "section": "Emphasis and strong emphasis" }, { "markdown": "**a\n", "html": "

**ahttp://foo.bar/?q=**

\n", "example": 479, "start_line": 7433, "end_line": 7437, "section": "Emphasis and strong emphasis" }, { "markdown": "__a\n", "html": "

__ahttp://foo.bar/?q=__

\n", "example": 480, "start_line": 7440, "end_line": 7444, "section": "Emphasis and strong emphasis" }, { "markdown": "[link](/uri \"title\")\n", "html": "

link

\n", "example": 481, "start_line": 7528, "end_line": 7532, "section": "Links" }, { "markdown": "[link](/uri)\n", "html": "

link

\n", "example": 482, "start_line": 7538, "end_line": 7542, "section": "Links" }, { "markdown": "[](./target.md)\n", "html": "

\n", "example": 483, "start_line": 7544, "end_line": 7548, "section": "Links" }, { "markdown": "[link]()\n", "html": "

link

\n", "example": 484, "start_line": 7551, "end_line": 7555, "section": "Links" }, { "markdown": "[link](<>)\n", "html": "

link

\n", "example": 485, "start_line": 7558, "end_line": 7562, "section": "Links" }, { "markdown": "[]()\n", "html": "

\n", "example": 486, "start_line": 7565, "end_line": 7569, "section": "Links" }, { "markdown": "[link](/my uri)\n", "html": "

[link](/my uri)

\n", "example": 487, "start_line": 7574, "end_line": 7578, "section": "Links" }, { "markdown": "[link](
)\n", "html": "

link

\n", "example": 488, "start_line": 7580, "end_line": 7584, "section": "Links" }, { "markdown": "[link](foo\nbar)\n", "html": "

[link](foo\nbar)

\n", "example": 489, "start_line": 7589, "end_line": 7595, "section": "Links" }, { "markdown": "[link]()\n", "html": "

[link]()

\n", "example": 490, "start_line": 7597, "end_line": 7603, "section": "Links" }, { "markdown": "[a]()\n", "html": "

a

\n", "example": 491, "start_line": 7608, "end_line": 7612, "section": "Links" }, { "markdown": "[link]()\n", "html": "

[link](<foo>)

\n", "example": 492, "start_line": 7616, "end_line": 7620, "section": "Links" }, { "markdown": "[a](\n[a](c)\n", "html": "

[a](<b)c\n[a](<b)c>\n[a](c)

\n", "example": 493, "start_line": 7625, "end_line": 7633, "section": "Links" }, { "markdown": "[link](\\(foo\\))\n", "html": "

link

\n", "example": 494, "start_line": 7637, "end_line": 7641, "section": "Links" }, { "markdown": "[link](foo(and(bar)))\n", "html": "

link

\n", "example": 495, "start_line": 7646, "end_line": 7650, "section": "Links" }, { "markdown": "[link](foo(and(bar))\n", "html": "

[link](foo(and(bar))

\n", "example": 496, "start_line": 7655, "end_line": 7659, "section": "Links" }, { "markdown": "[link](foo\\(and\\(bar\\))\n", "html": "

link

\n", "example": 497, "start_line": 7662, "end_line": 7666, "section": "Links" }, { "markdown": "[link]()\n", "html": "

link

\n", "example": 498, "start_line": 7669, "end_line": 7673, "section": "Links" }, { "markdown": "[link](foo\\)\\:)\n", "html": "

link

\n", "example": 499, "start_line": 7679, "end_line": 7683, "section": "Links" }, { "markdown": "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n", "html": "

link

\n

link

\n

link

\n", "example": 500, "start_line": 7688, "end_line": 7698, "section": "Links" }, { "markdown": "[link](foo\\bar)\n", "html": "

link

\n", "example": 501, "start_line": 7704, "end_line": 7708, "section": "Links" }, { "markdown": "[link](foo%20bä)\n", "html": "

link

\n", "example": 502, "start_line": 7720, "end_line": 7724, "section": "Links" }, { "markdown": "[link](\"title\")\n", "html": "

link

\n", "example": 503, "start_line": 7731, "end_line": 7735, "section": "Links" }, { "markdown": "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n", "html": "

link\nlink\nlink

\n", "example": 504, "start_line": 7740, "end_line": 7748, "section": "Links" }, { "markdown": "[link](/url \"title \\\""\")\n", "html": "

link

\n", "example": 505, "start_line": 7754, "end_line": 7758, "section": "Links" }, { "markdown": "[link](/url \"title\")\n", "html": "

link

\n", "example": 506, "start_line": 7765, "end_line": 7769, "section": "Links" }, { "markdown": "[link](/url \"title \"and\" title\")\n", "html": "

[link](/url "title "and" title")

\n", "example": 507, "start_line": 7774, "end_line": 7778, "section": "Links" }, { "markdown": "[link](/url 'title \"and\" title')\n", "html": "

link

\n", "example": 508, "start_line": 7783, "end_line": 7787, "section": "Links" }, { "markdown": "[link]( /uri\n \"title\" )\n", "html": "

link

\n", "example": 509, "start_line": 7808, "end_line": 7813, "section": "Links" }, { "markdown": "[link] (/uri)\n", "html": "

[link] (/uri)

\n", "example": 510, "start_line": 7819, "end_line": 7823, "section": "Links" }, { "markdown": "[link [foo [bar]]](/uri)\n", "html": "

link [foo [bar]]

\n", "example": 511, "start_line": 7829, "end_line": 7833, "section": "Links" }, { "markdown": "[link] bar](/uri)\n", "html": "

[link] bar](/uri)

\n", "example": 512, "start_line": 7836, "end_line": 7840, "section": "Links" }, { "markdown": "[link [bar](/uri)\n", "html": "

[link bar

\n", "example": 513, "start_line": 7843, "end_line": 7847, "section": "Links" }, { "markdown": "[link \\[bar](/uri)\n", "html": "

link [bar

\n", "example": 514, "start_line": 7850, "end_line": 7854, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*](/uri)\n", "html": "

link foo bar #

\n", "example": 515, "start_line": 7859, "end_line": 7863, "section": "Links" }, { "markdown": "[![moon](moon.jpg)](/uri)\n", "html": "

\"moon\"

\n", "example": 516, "start_line": 7866, "end_line": 7870, "section": "Links" }, { "markdown": "[foo [bar](/uri)](/uri)\n", "html": "

[foo bar](/uri)

\n", "example": 517, "start_line": 7875, "end_line": 7879, "section": "Links" }, { "markdown": "[foo *[bar [baz](/uri)](/uri)*](/uri)\n", "html": "

[foo [bar baz](/uri)](/uri)

\n", "example": 518, "start_line": 7882, "end_line": 7886, "section": "Links" }, { "markdown": "![[[foo](uri1)](uri2)](uri3)\n", "html": "

\"[foo](uri2)\"

\n", "example": 519, "start_line": 7889, "end_line": 7893, "section": "Links" }, { "markdown": "*[foo*](/uri)\n", "html": "

*foo*

\n", "example": 520, "start_line": 7899, "end_line": 7903, "section": "Links" }, { "markdown": "[foo *bar](baz*)\n", "html": "

foo *bar

\n", "example": 521, "start_line": 7906, "end_line": 7910, "section": "Links" }, { "markdown": "*foo [bar* baz]\n", "html": "

foo [bar baz]

\n", "example": 522, "start_line": 7916, "end_line": 7920, "section": "Links" }, { "markdown": "[foo \n", "html": "

[foo

\n", "example": 523, "start_line": 7926, "end_line": 7930, "section": "Links" }, { "markdown": "[foo`](/uri)`\n", "html": "

[foo](/uri)

\n", "example": 524, "start_line": 7933, "end_line": 7937, "section": "Links" }, { "markdown": "[foo\n", "html": "

[foohttp://example.com/?search=](uri)

\n", "example": 525, "start_line": 7940, "end_line": 7944, "section": "Links" }, { "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n", "html": "

foo

\n", "example": 526, "start_line": 7978, "end_line": 7984, "section": "Links" }, { "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n", "html": "

link [foo [bar]]

\n", "example": 527, "start_line": 7993, "end_line": 7999, "section": "Links" }, { "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n", "html": "

link [bar

\n", "example": 528, "start_line": 8002, "end_line": 8008, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n", "html": "

link foo bar #

\n", "example": 529, "start_line": 8013, "end_line": 8019, "section": "Links" }, { "markdown": "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n", "html": "

\"moon\"

\n", "example": 530, "start_line": 8022, "end_line": 8028, "section": "Links" }, { "markdown": "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n", "html": "

[foo bar]ref

\n", "example": 531, "start_line": 8033, "end_line": 8039, "section": "Links" }, { "markdown": "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n", "html": "

[foo bar baz]ref

\n", "example": 532, "start_line": 8042, "end_line": 8048, "section": "Links" }, { "markdown": "*[foo*][ref]\n\n[ref]: /uri\n", "html": "

*foo*

\n", "example": 533, "start_line": 8057, "end_line": 8063, "section": "Links" }, { "markdown": "[foo *bar][ref]*\n\n[ref]: /uri\n", "html": "

foo *bar*

\n", "example": 534, "start_line": 8066, "end_line": 8072, "section": "Links" }, { "markdown": "[foo \n\n[ref]: /uri\n", "html": "

[foo

\n", "example": 535, "start_line": 8078, "end_line": 8084, "section": "Links" }, { "markdown": "[foo`][ref]`\n\n[ref]: /uri\n", "html": "

[foo][ref]

\n", "example": 536, "start_line": 8087, "end_line": 8093, "section": "Links" }, { "markdown": "[foo\n\n[ref]: /uri\n", "html": "

[foohttp://example.com/?search=][ref]

\n", "example": 537, "start_line": 8096, "end_line": 8102, "section": "Links" }, { "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n", "html": "

foo

\n", "example": 538, "start_line": 8107, "end_line": 8113, "section": "Links" }, { "markdown": "[ẞ]\n\n[SS]: /url\n", "html": "

\n", "example": 539, "start_line": 8118, "end_line": 8124, "section": "Links" }, { "markdown": "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n", "html": "

Baz

\n", "example": 540, "start_line": 8130, "end_line": 8137, "section": "Links" }, { "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n", "html": "

[foo] bar

\n", "example": 541, "start_line": 8143, "end_line": 8149, "section": "Links" }, { "markdown": "[foo]\n[bar]\n\n[bar]: /url \"title\"\n", "html": "

[foo]\nbar

\n", "example": 542, "start_line": 8152, "end_line": 8160, "section": "Links" }, { "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n", "html": "

bar

\n", "example": 543, "start_line": 8193, "end_line": 8201, "section": "Links" }, { "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n", "html": "

[bar][foo!]

\n", "example": 544, "start_line": 8208, "end_line": 8214, "section": "Links" }, { "markdown": "[foo][ref[]\n\n[ref[]: /uri\n", "html": "

[foo][ref[]

\n

[ref[]: /uri

\n", "example": 545, "start_line": 8220, "end_line": 8227, "section": "Links" }, { "markdown": "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n", "html": "

[foo][ref[bar]]

\n

[ref[bar]]: /uri

\n", "example": 546, "start_line": 8230, "end_line": 8237, "section": "Links" }, { "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n", "html": "

[[[foo]]]

\n

[[[foo]]]: /url

\n", "example": 547, "start_line": 8240, "end_line": 8247, "section": "Links" }, { "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n", "html": "

foo

\n", "example": 548, "start_line": 8250, "end_line": 8256, "section": "Links" }, { "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n", "html": "

bar\\

\n", "example": 549, "start_line": 8261, "end_line": 8267, "section": "Links" }, { "markdown": "[]\n\n[]: /uri\n", "html": "

[]

\n

[]: /uri

\n", "example": 550, "start_line": 8273, "end_line": 8280, "section": "Links" }, { "markdown": "[\n ]\n\n[\n ]: /uri\n", "html": "

[\n]

\n

[\n]: /uri

\n", "example": 551, "start_line": 8283, "end_line": 8294, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url \"title\"\n", "html": "

foo

\n", "example": 552, "start_line": 8306, "end_line": 8312, "section": "Links" }, { "markdown": "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "

foo bar

\n", "example": 553, "start_line": 8315, "end_line": 8321, "section": "Links" }, { "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n", "html": "

Foo

\n", "example": 554, "start_line": 8326, "end_line": 8332, "section": "Links" }, { "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "

foo\n[]

\n", "example": 555, "start_line": 8339, "end_line": 8347, "section": "Links" }, { "markdown": "[foo]\n\n[foo]: /url \"title\"\n", "html": "

foo

\n", "example": 556, "start_line": 8359, "end_line": 8365, "section": "Links" }, { "markdown": "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "

foo bar

\n", "example": 557, "start_line": 8368, "end_line": 8374, "section": "Links" }, { "markdown": "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n", "html": "

[foo bar]

\n", "example": 558, "start_line": 8377, "end_line": 8383, "section": "Links" }, { "markdown": "[[bar [foo]\n\n[foo]: /url\n", "html": "

[[bar foo

\n", "example": 559, "start_line": 8386, "end_line": 8392, "section": "Links" }, { "markdown": "[Foo]\n\n[foo]: /url \"title\"\n", "html": "

Foo

\n", "example": 560, "start_line": 8397, "end_line": 8403, "section": "Links" }, { "markdown": "[foo] bar\n\n[foo]: /url\n", "html": "

foo bar

\n", "example": 561, "start_line": 8408, "end_line": 8414, "section": "Links" }, { "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n", "html": "

[foo]

\n", "example": 562, "start_line": 8420, "end_line": 8426, "section": "Links" }, { "markdown": "[foo*]: /url\n\n*[foo*]\n", "html": "

*foo*

\n", "example": 563, "start_line": 8432, "end_line": 8438, "section": "Links" }, { "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n", "html": "

foo

\n", "example": 564, "start_line": 8444, "end_line": 8451, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url1\n", "html": "

foo

\n", "example": 565, "start_line": 8453, "end_line": 8459, "section": "Links" }, { "markdown": "[foo]()\n\n[foo]: /url1\n", "html": "

foo

\n", "example": 566, "start_line": 8463, "end_line": 8469, "section": "Links" }, { "markdown": "[foo](not a link)\n\n[foo]: /url1\n", "html": "

foo(not a link)

\n", "example": 567, "start_line": 8471, "end_line": 8477, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url\n", "html": "

[foo]bar

\n", "example": 568, "start_line": 8482, "end_line": 8488, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n", "html": "

foobaz

\n", "example": 569, "start_line": 8494, "end_line": 8501, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n", "html": "

[foo]bar

\n", "example": 570, "start_line": 8507, "end_line": 8514, "section": "Links" }, { "markdown": "![foo](/url \"title\")\n", "html": "

\"foo\"

\n", "example": 571, "start_line": 8530, "end_line": 8534, "section": "Images" }, { "markdown": "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "

\"foo

\n", "example": 572, "start_line": 8537, "end_line": 8543, "section": "Images" }, { "markdown": "![foo ![bar](/url)](/url2)\n", "html": "

\"foo

\n", "example": 573, "start_line": 8546, "end_line": 8550, "section": "Images" }, { "markdown": "![foo [bar](/url)](/url2)\n", "html": "

\"foo

\n", "example": 574, "start_line": 8553, "end_line": 8557, "section": "Images" }, { "markdown": "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "

\"foo

\n", "example": 575, "start_line": 8567, "end_line": 8573, "section": "Images" }, { "markdown": "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n", "html": "

\"foo

\n", "example": 576, "start_line": 8576, "end_line": 8582, "section": "Images" }, { "markdown": "![foo](train.jpg)\n", "html": "

\"foo\"

\n", "example": 577, "start_line": 8585, "end_line": 8589, "section": "Images" }, { "markdown": "My ![foo bar](/path/to/train.jpg \"title\" )\n", "html": "

My \"foo

\n", "example": 578, "start_line": 8592, "end_line": 8596, "section": "Images" }, { "markdown": "![foo]()\n", "html": "

\"foo\"

\n", "example": 579, "start_line": 8599, "end_line": 8603, "section": "Images" }, { "markdown": "![](/url)\n", "html": "

\"\"

\n", "example": 580, "start_line": 8606, "end_line": 8610, "section": "Images" }, { "markdown": "![foo][bar]\n\n[bar]: /url\n", "html": "

\"foo\"

\n", "example": 581, "start_line": 8615, "end_line": 8621, "section": "Images" }, { "markdown": "![foo][bar]\n\n[BAR]: /url\n", "html": "

\"foo\"

\n", "example": 582, "start_line": 8624, "end_line": 8630, "section": "Images" }, { "markdown": "![foo][]\n\n[foo]: /url \"title\"\n", "html": "

\"foo\"

\n", "example": 583, "start_line": 8635, "end_line": 8641, "section": "Images" }, { "markdown": "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "

\"foo

\n", "example": 584, "start_line": 8644, "end_line": 8650, "section": "Images" }, { "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n", "html": "

\"Foo\"

\n", "example": 585, "start_line": 8655, "end_line": 8661, "section": "Images" }, { "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "

\"foo\"\n[]

\n", "example": 586, "start_line": 8667, "end_line": 8675, "section": "Images" }, { "markdown": "![foo]\n\n[foo]: /url \"title\"\n", "html": "

\"foo\"

\n", "example": 587, "start_line": 8680, "end_line": 8686, "section": "Images" }, { "markdown": "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "

\"foo

\n", "example": 588, "start_line": 8689, "end_line": 8695, "section": "Images" }, { "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n", "html": "

![[foo]]

\n

[[foo]]: /url "title"

\n", "example": 589, "start_line": 8700, "end_line": 8707, "section": "Images" }, { "markdown": "![Foo]\n\n[foo]: /url \"title\"\n", "html": "

\"Foo\"

\n", "example": 590, "start_line": 8712, "end_line": 8718, "section": "Images" }, { "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n", "html": "

![foo]

\n", "example": 591, "start_line": 8724, "end_line": 8730, "section": "Images" }, { "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n", "html": "

!foo

\n", "example": 592, "start_line": 8736, "end_line": 8742, "section": "Images" }, { "markdown": "\n", "html": "

http://foo.bar.baz

\n", "example": 593, "start_line": 8769, "end_line": 8773, "section": "Autolinks" }, { "markdown": "\n", "html": "

http://foo.bar.baz/test?q=hello&id=22&boolean

\n", "example": 594, "start_line": 8776, "end_line": 8780, "section": "Autolinks" }, { "markdown": "\n", "html": "

irc://foo.bar:2233/baz

\n", "example": 595, "start_line": 8783, "end_line": 8787, "section": "Autolinks" }, { "markdown": "\n", "html": "

MAILTO:FOO@BAR.BAZ

\n", "example": 596, "start_line": 8792, "end_line": 8796, "section": "Autolinks" }, { "markdown": "\n", "html": "

a+b+c:d

\n", "example": 597, "start_line": 8804, "end_line": 8808, "section": "Autolinks" }, { "markdown": "\n", "html": "

made-up-scheme://foo,bar

\n", "example": 598, "start_line": 8811, "end_line": 8815, "section": "Autolinks" }, { "markdown": "\n", "html": "

http://../

\n", "example": 599, "start_line": 8818, "end_line": 8822, "section": "Autolinks" }, { "markdown": "\n", "html": "

localhost:5001/foo

\n", "example": 600, "start_line": 8825, "end_line": 8829, "section": "Autolinks" }, { "markdown": "\n", "html": "

<http://foo.bar/baz bim>

\n", "example": 601, "start_line": 8834, "end_line": 8838, "section": "Autolinks" }, { "markdown": "\n", "html": "

http://example.com/\\[\\

\n", "example": 602, "start_line": 8843, "end_line": 8847, "section": "Autolinks" }, { "markdown": "\n", "html": "

foo@bar.example.com

\n", "example": 603, "start_line": 8865, "end_line": 8869, "section": "Autolinks" }, { "markdown": "\n", "html": "

foo+special@Bar.baz-bar0.com

\n", "example": 604, "start_line": 8872, "end_line": 8876, "section": "Autolinks" }, { "markdown": "\n", "html": "

<foo+@bar.example.com>

\n", "example": 605, "start_line": 8881, "end_line": 8885, "section": "Autolinks" }, { "markdown": "<>\n", "html": "

<>

\n", "example": 606, "start_line": 8890, "end_line": 8894, "section": "Autolinks" }, { "markdown": "< http://foo.bar >\n", "html": "

< http://foo.bar >

\n", "example": 607, "start_line": 8897, "end_line": 8901, "section": "Autolinks" }, { "markdown": "\n", "html": "

<m:abc>

\n", "example": 608, "start_line": 8904, "end_line": 8908, "section": "Autolinks" }, { "markdown": "\n", "html": "

<foo.bar.baz>

\n", "example": 609, "start_line": 8911, "end_line": 8915, "section": "Autolinks" }, { "markdown": "http://example.com\n", "html": "

http://example.com

\n", "example": 610, "start_line": 8918, "end_line": 8922, "section": "Autolinks" }, { "markdown": "foo@bar.example.com\n", "html": "

foo@bar.example.com

\n", "example": 611, "start_line": 8925, "end_line": 8929, "section": "Autolinks" }, { "markdown": "\n", "html": "

\n", "example": 612, "start_line": 9006, "end_line": 9010, "section": "Raw HTML" }, { "markdown": "\n", "html": "

\n", "example": 613, "start_line": 9015, "end_line": 9019, "section": "Raw HTML" }, { "markdown": "\n", "html": "

\n", "example": 614, "start_line": 9024, "end_line": 9030, "section": "Raw HTML" }, { "markdown": "\n", "html": "

\n", "example": 615, "start_line": 9035, "end_line": 9041, "section": "Raw HTML" }, { "markdown": "Foo \n", "html": "

Foo

\n", "example": 616, "start_line": 9046, "end_line": 9050, "section": "Raw HTML" }, { "markdown": "<33> <__>\n", "html": "

<33> <__>

\n", "example": 617, "start_line": 9055, "end_line": 9059, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

<a h*#ref="hi">

\n", "example": 618, "start_line": 9064, "end_line": 9068, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

<a href="hi'> <a href=hi'>

\n", "example": 619, "start_line": 9073, "end_line": 9077, "section": "Raw HTML" }, { "markdown": "< a><\nfoo>\n\n", "html": "

< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />

\n", "example": 620, "start_line": 9082, "end_line": 9092, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

<a href='bar'title=title>

\n", "example": 621, "start_line": 9097, "end_line": 9101, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

\n", "example": 622, "start_line": 9106, "end_line": 9110, "section": "Raw HTML" }, { "markdown": "\n", "html": "

</a href="foo">

\n", "example": 623, "start_line": 9115, "end_line": 9119, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", "example": 624, "start_line": 9124, "end_line": 9130, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo <!-- not a comment -- two hyphens -->

\n", "example": 625, "start_line": 9133, "end_line": 9137, "section": "Raw HTML" }, { "markdown": "foo foo -->\n\nfoo \n", "html": "

foo <!--> foo -->

\n

foo <!-- foo--->

\n", "example": 626, "start_line": 9142, "end_line": 9149, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", "example": 627, "start_line": 9154, "end_line": 9158, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", "example": 628, "start_line": 9163, "end_line": 9167, "section": "Raw HTML" }, { "markdown": "foo &<]]>\n", "html": "

foo &<]]>

\n", "example": 629, "start_line": 9172, "end_line": 9176, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", "example": 630, "start_line": 9182, "end_line": 9186, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", "example": 631, "start_line": 9191, "end_line": 9195, "section": "Raw HTML" }, { "markdown": "\n", "html": "

<a href=""">

\n", "example": 632, "start_line": 9198, "end_line": 9202, "section": "Raw HTML" }, { "markdown": "foo \nbaz\n", "html": "

foo
\nbaz

\n", "example": 633, "start_line": 9212, "end_line": 9218, "section": "Hard line breaks" }, { "markdown": "foo\\\nbaz\n", "html": "

foo
\nbaz

\n", "example": 634, "start_line": 9224, "end_line": 9230, "section": "Hard line breaks" }, { "markdown": "foo \nbaz\n", "html": "

foo
\nbaz

\n", "example": 635, "start_line": 9235, "end_line": 9241, "section": "Hard line breaks" }, { "markdown": "foo \n bar\n", "html": "

foo
\nbar

\n", "example": 636, "start_line": 9246, "end_line": 9252, "section": "Hard line breaks" }, { "markdown": "foo\\\n bar\n", "html": "

foo
\nbar

\n", "example": 637, "start_line": 9255, "end_line": 9261, "section": "Hard line breaks" }, { "markdown": "*foo \nbar*\n", "html": "

foo
\nbar

\n", "example": 638, "start_line": 9267, "end_line": 9273, "section": "Hard line breaks" }, { "markdown": "*foo\\\nbar*\n", "html": "

foo
\nbar

\n", "example": 639, "start_line": 9276, "end_line": 9282, "section": "Hard line breaks" }, { "markdown": "`code \nspan`\n", "html": "

code span

\n", "example": 640, "start_line": 9287, "end_line": 9292, "section": "Hard line breaks" }, { "markdown": "`code\\\nspan`\n", "html": "

code\\ span

\n", "example": 641, "start_line": 9295, "end_line": 9300, "section": "Hard line breaks" }, { "markdown": "
\n", "html": "

\n", "example": 642, "start_line": 9305, "end_line": 9311, "section": "Hard line breaks" }, { "markdown": "\n", "html": "

\n", "example": 643, "start_line": 9314, "end_line": 9320, "section": "Hard line breaks" }, { "markdown": "foo\\\n", "html": "

foo\\

\n", "example": 644, "start_line": 9327, "end_line": 9331, "section": "Hard line breaks" }, { "markdown": "foo \n", "html": "

foo

\n", "example": 645, "start_line": 9334, "end_line": 9338, "section": "Hard line breaks" }, { "markdown": "### foo\\\n", "html": "

foo\\

\n", "example": 646, "start_line": 9341, "end_line": 9345, "section": "Hard line breaks" }, { "markdown": "### foo \n", "html": "

foo

\n", "example": 647, "start_line": 9348, "end_line": 9352, "section": "Hard line breaks" }, { "markdown": "foo\nbaz\n", "html": "

foo\nbaz

\n", "example": 648, "start_line": 9363, "end_line": 9369, "section": "Soft line breaks" }, { "markdown": "foo \n baz\n", "html": "

foo\nbaz

\n", "example": 649, "start_line": 9375, "end_line": 9381, "section": "Soft line breaks" }, { "markdown": "hello $.;'there\n", "html": "

hello $.;'there

\n", "example": 650, "start_line": 9395, "end_line": 9399, "section": "Textual content" }, { "markdown": "Foo χρῆν\n", "html": "

Foo χρῆν

\n", "example": 651, "start_line": 9402, "end_line": 9406, "section": "Textual content" }, { "markdown": "Multiple spaces\n", "html": "

Multiple spaces

\n", "example": 652, "start_line": 9411, "end_line": 9415, "section": "Textual content" } ] mdformat-0.7.22/tests/data/consecutive_numbering.md000066400000000000000000000006711475015777400224320ustar00rootroot00000000000000numbered lists [consecutive] . 1. a 2. b 3. c d . 1. a 2. b 3. c d . numbered lists starting number [consecutive] . 099. a 100. b 101. c d . 099. a 100. b 101. c d . numbered lists nested [consecutive] . 1. a 2. b 1. x 2. y z . 1. a 2. b 1. x 2. y z . 0 prefix . Prefix needed: 999. a 1000. b No prefix needed: 998. a 999. b . Prefix needed: 0999. a 1000. b No prefix needed: 998. a 999. b . mdformat-0.7.22/tests/data/default_style.md000066400000000000000000000137711475015777400207060ustar00rootroot00000000000000strip paragraph lines . trailing whitespace at the end of paragraph lines should be stripped . trailing whitespace at the end of paragraph lines should be stripped . strip quotes . > Paragraph 1 > > Paragraph 2 . > Paragraph 1 > > Paragraph 2 . no escape ampersand . R&B, rock & roll . R&B, rock & roll . list whitespaces . - item one - item two - sublist - sublist . - item one - item two - sublist - sublist . convert setext to ATX heading . Top level heading ========= 2nd level heading --------- . # Top level heading ## 2nd level heading . Lists with different bullets . - a - b * c . - a - b * c . numbered lists . 1. a 2. b 3. c d . 1. a 1. b 1. c d . numbered lists starting number . 099. a 100. b 101. c d . 99. a 01. b 01. c d . numbered lists nested . 1. a 2. b 1. x 2. y z . 1. a 1. b 1. x 1. y z . references: . [ref2]: link3 "title" [text](link1) [text](link2 "title") [ref1] [ref2] [text][ref1] ![text](link1) ![text](link2 "title") ![ref1] ![ref2] ![text][ref1] [ref1]: link4 [unused]: link5 . [text](link1) [text](link2 "title") [ref1] [ref2] [text][ref1] ![text](link1) ![text](link2 "title") ![ref1] ![ref2] ![text][ref1] [ref1]: link4 [ref2]: link3 "title" . thematic breaks . something something --- something something . something something ______________________________________________________________________ something something . Ordered list marker type . 1) a 2) b 1. x 2. y . 1. a 1. b 1) x 1) y . Bullet list marker type . * a * b + x + y - c - d + e + f . - a - b * x * y - c - d * e * f . Empty list item . - next item is empty - - whitespace should be stripped 1. next item is empty 1. 1. whitespace should be stripped . - next item is empty - - whitespace should be stripped 1. next item is empty 1. 1. whitespace should be stripped . Empty ref link destination . [foo]: <> [foo] . [foo] [foo]: <> . Don't escape hash . - Recalculate secondary dependencies between rounds (#378) . - Recalculate secondary dependencies between rounds (#378) . Only escape first ")" and "." . 1\) Only the first "\)" of a line should be escaped in this paragraph. 1\. Only the first "\." of a line should be escaped in this paragraph. First \. or \) char should not be escaped here because this line does not look like a list. . 1\) Only the first ")" of a line should be escaped in this paragraph. 1\. Only the first "." of a line should be escaped in this paragraph. First . or ) char should not be escaped here because this line does not look like a list. . Don't escape list item marker if not followed by whitespace or end of line . 1.No need to escape the dot here 1)No need to escape the closing bracket here 1. No need to escape the dot here (there is a no-break-space, not space) This needs escaping (end of line after the dot) 1. This needs escaping (space after the closing bracket) 1) . 1.No need to escape the dot here 1)No need to escape the closing bracket here 1. No need to escape the dot here (there is a no-break-space, not space) This needs escaping (end of line after the dot) 1\. This needs escaping (space after the closing bracket) 1\) . Escape line starting "+" and "-" . \+ \+No need to escape plus +No need to escape plus \- \-No need to escape dash -No need to escape dash . \+ +No need to escape plus +No need to escape plus \- -No need to escape dash -No need to escape dash . Escape ! preceding a link . We must escape the exclamation here \![link](https://www.debian.org/)!!! . We must escape the exclamation here \![link](https://www.debian.org/)!!! . Asterisk escapes . Escape*asterisk\* Don't * escape * asterisk . Escape\*asterisk\* Don't * escape * asterisk . Underscore escapes . Do _escape Don't esc_ape Don't _ escape _ underscore . Do \_escape Don't esc_ape Don't _ escape _ underscore . Keep shortcut reference links (dont convert to full reference) . ![Image] [iMaGe]: train.jpg [Foo] [fOO]: /url "title" . ![Image] [Foo] [foo]: /url "title" [image]: train.jpg . Empty document has no newline . . . Don't escape hash not followed by space . #No space after hash -> no need to escape . #No space after hash -> no need to escape . Indented raw HTML contains Markdown .
- list item
.
- list item
. Autolink with percentage encoded space . . . Keep mailto: prefix in autolink . > . > . Tilde in info string . ```~/.gitconfig [user] ``` . ```~/.gitconfig [user] ``` . Info string starts with tilde and contains backtick . ~~~\~` 123 ~~~ . ~~~\~` 123 ~~~ . Keep inline HTML as inline, and block HTML as block . A
A
. A
A
. Keep HTML blocks of type 7 unindented (they do not interrupt a paragraph) . text
text . text
text . sort digit references numerically . (References)[1] (should)[2] (sort)[10] (numerically)[word], if[🤪] they[⅕] are[𐩂] digits[!]. [🤪]: . [2]: . [word]: . [10]: . [⅕]: . [1]: . [𐩂]: . [!]: . . (References)[1] (should)[2] (sort)[10] (numerically)[word], if[🤪] they[⅕] are[𐩂] digits[!]. [!]: . [1]: . [2]: . [10]: . [word]: . [⅕]: . [𐩂]: . [🤪]: . . Unicode space (U+3000) after heading . # hoge   . # hoge . Square bracket escapes . [no-escape]no [no-escape] no [\[\]](/url) [escape] [inline\](/url) [link-label] [link-label\] [link-label\]: /url [link-label]: /url . [no-escape]no [no-escape] no [[]](/url) [escape] \[inline\](/url) [link-label] \[link-label\] \[link-label\]: /url [link-label]: /url . Less than sign escapes . < no escape < no escape, now escape < < < . < no escape < no escape, now escape \< \< \< . Tabs to spaces . # Convert tab to space in headings Make a space here. . # Convert tab to space in headings Make a space here. . Reduce tabs and spaces to one space . # Reduce in headings Reduce to a space here. . # Reduce in headings Reduce to a space here. . mdformat-0.7.22/tests/data/wrap_width_50.md000066400000000000000000000114071475015777400205100ustar00rootroot00000000000000Wrap before no-wrap section . We want to wrap before the inline code block `THIS IS THE LONG INLINE CODE BLOCK. IT SHOULD BE ON ITS OWN LINE`. No need to wrap before the emphasis section _THIS IS THE LONG EMPHASIS SECTION. ITS CONTENT SHOULD BE WRAPPED LIKE NORMAL TEXT_ We want to wrap before the link [THIS IS THE LINK THAT SHOULD BE ON ITS OWN LINE](https://www.python.org/) . We want to wrap before the inline code block `THIS IS THE LONG INLINE CODE BLOCK. IT SHOULD BE ON ITS OWN LINE`. No need to wrap before the emphasis section _THIS IS THE LONG EMPHASIS SECTION. ITS CONTENT SHOULD BE WRAPPED LIKE NORMAL TEXT_ We want to wrap before the link [THIS IS THE LINK THAT SHOULD BE ON ITS OWN LINE](https://www.python.org/) . Wrap boundary . z zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz z zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz . z zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz z zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz . No newline in image description . Blii blaa. There is no newline in this image ![here it is](https://github.com/executablebooks/) . Blii blaa. There is no newline in this image ![here it is](https://github.com/executablebooks/) . No newline in link text . Blii blaa. There is no newline in this link [here it is](https://github.com/executablebooks/) . Blii blaa. There is no newline in this link [here it is](https://github.com/executablebooks/) . Max width lines . Brackets are always dedented and that a trailing Aproduces smaller diffs; when you add or remove an So, having the closing bracket dedented provides a Sections of the code that otherwise share the same List and the docstring in the example above). . Brackets are always dedented and that a trailing Aproduces smaller diffs; when you add or remove an So, having the closing bracket dedented provides a Sections of the code that otherwise share the same List and the docstring in the example above). . No space between normal section and no-break section . As around `:` operators for "simple expressions" (`ham[lower:upper]`), and extra. . As around `:` operators for "simple expressions" (`ham[lower:upper]`), and extra. . Hard break in emphasized link . _[hard\ break](python.org)_ . _[hard\ break](python.org)_ . Link in emphasis . _[do not add line breaks in a link. That's the style, at least currently](python.org)_ . _[do not add line breaks in a link. That's the style, at least currently](python.org)_ . Indented blocks . no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a - no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a - do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a 10. no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a 11. do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a > no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a > do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a . no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a - no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a - do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a 10. no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a 01. do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a > no wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a > do wrap aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa > a . Only use space, tab and line feed as wrap points . aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa   No-wrap-points-until-now: Unicode whitespace shouldnt act as wrap point, the normal space should . aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa   No-wrap-points-until-now: Unicode whitespace shouldnt act as wrap point, the normal space should . Hard break (issue #326) . Loremiiinoeffefe\ ipsum dolor sit amet, consectetur adip elitismisun. Here's 50 chars including hardbreak backslashhhhh\ ipsum dolor sit amet, consectetur adip elitismisun. Here's 50 chars before hardbreak backslash ggggggg\ ipsum dolor sit amet, consectetur adip elitismisun. . Loremiiinoeffefe\ ipsum dolor sit amet, consectetur adip elitismisun. Here's 50 chars including hardbreak backslashhhhh\ ipsum dolor sit amet, consectetur adip elitismisun. Here's 50 chars before hardbreak backslash ggggggg\ ipsum dolor sit amet, consectetur adip elitismisun. . Newline in HTML inline . There is no hard break here, only HTML inlineee
ipsum dolor sit amet, consectetur adip elitismisun. sssssssssssssssssssssssssssssssssssss backslash ipsum dolor sit amet, consectetur adip elitismisun. . There is no hard break here, only HTML inlineee ipsum dolor sit amet, consectetur adip elitismisun. sssssssssssssssssssssssssssssssssssss backslash ipsum dolor sit amet, consectetur adip elitismisun. . Starts with encoded space . * * * . * * \* . mdformat-0.7.22/tests/requirements.txt000066400000000000000000000000561475015777400200630ustar00rootroot00000000000000pytest pytest-randomly pytest-cov covdefaults mdformat-0.7.22/tests/test_api.py000066400000000000000000000104421475015777400167610ustar00rootroot00000000000000import os from markdown_it import MarkdownIt import pytest import mdformat from mdformat._util import is_md_equal from mdformat.renderer import MDRenderer UNFORMATTED_MARKDOWN = "\n\n# A header\n\n" FORMATTED_MARKDOWN = "# A header\n" def test_fmt_file(tmp_path): file_path = tmp_path / "test_markdown.md" # Use string argument file_path.write_text(UNFORMATTED_MARKDOWN) mdformat.file(str(file_path)) assert file_path.read_text() == FORMATTED_MARKDOWN # Use pathlib.Path argument file_path.write_text(UNFORMATTED_MARKDOWN) mdformat.file(file_path) assert file_path.read_text() == FORMATTED_MARKDOWN def test_fmt_file__invalid_filename(): with pytest.raises(ValueError) as exc_info: mdformat.file("this is not a valid filepath?`=|><@{[]\\/,.%¤#'") assert "not a file" in str(exc_info.value) def test_fmt_file__symlink(tmp_path): file_path = tmp_path / "test_markdown.md" file_path.write_text(UNFORMATTED_MARKDOWN) symlink_path = tmp_path / "symlink.md" symlink_path.symlink_to(file_path) with pytest.raises(ValueError) as exc_info: mdformat.file(symlink_path) assert "It is a symlink" in str(exc_info.value) def test_fmt_string(): assert mdformat.text(UNFORMATTED_MARKDOWN) == FORMATTED_MARKDOWN @pytest.mark.parametrize( "input_", [ pytest.param("\x0b"), # vertical tab only pytest.param("\t##"), # no trailing newline in codeblock pytest.param("a\n\n\xa0\n\nb"), # lone NBSP between two paragraphs pytest.param("\xa0\n\n# heading"), # lone NBSP followed by a heading pytest.param( "```\na\n```\n\u2003\n# A\n" ), # em space surrounded by code and header ], ) def test_output_is_equal(input_): output = mdformat.text(input_) assert is_md_equal(input_, output) @pytest.mark.parametrize( "input_", [ pytest.param("\x1c\n\na"), pytest.param(">\x0b"), pytest.param("<@{[]\\/,.%¤#'",)) assert exc_info.value.code == 2 captured = capsys.readouterr() assert "does not exist" in captured.err @pytest.mark.skipif(os.name == "nt", reason="No os.mkfifo on windows") def test_fifo(tmp_path, capsys): fifo_path = tmp_path / "fifo1" os.mkfifo(fifo_path) with pytest.raises(SystemExit) as exc_info: run((str(fifo_path),)) assert exc_info.value.code == 2 assert "does not exist" in capsys.readouterr().err def test_check(tmp_path): file_path = tmp_path / "test_markdown.md" file_path.write_bytes(FORMATTED_MARKDOWN.encode()) assert run((str(file_path), "--check")) == 0 def test_check__fail(tmp_path): file_path = tmp_path / "test_markdown.md" file_path.write_text(UNFORMATTED_MARKDOWN) assert run((str(file_path), "--check")) == 1 def test_check__multi_fail(capsys, tmp_path): """Test for --check flag when multiple files are unformatted. Test that the names of all unformatted files are listed when using --check. """ file_path1 = tmp_path / "test_markdown1.md" file_path2 = tmp_path / "test_markdown2.md" file_path1.write_text(UNFORMATTED_MARKDOWN) file_path2.write_text(UNFORMATTED_MARKDOWN) assert run((str(tmp_path), "--check")) == 1 captured = capsys.readouterr() assert str(file_path1) in captured.err assert str(file_path2) in captured.err def example_formatter(code, info): return "dummy\n" def test_formatter_plugin(tmp_path, monkeypatch): monkeypatch.setitem(CODEFORMATTERS, "lang", example_formatter) file_path = tmp_path / "test_markdown.md" file_path.write_text("```lang\nother\n```\n") assert run((str(file_path),)) == 0 assert file_path.read_text() == "```lang\ndummy\n```\n" def test_dash_stdin(capfd, monkeypatch): monkeypatch.setattr(sys, "stdin", StringIO(UNFORMATTED_MARKDOWN)) assert run(("-",)) == 0 captured = capfd.readouterr() assert captured.out == FORMATTED_MARKDOWN def test_wrap_paragraphs(): with patch("shutil.get_terminal_size", return_value=(72, 24)): assert wrap_paragraphs( [ 'Error: Could not format "/home/user/file_name_longer_than_wrap_width--------------------------------------.md".', # noqa: E501 "The formatted Markdown renders to different HTML than the input Markdown. " # noqa: E501 "This is likely a bug in mdformat. " "Please create an issue report here: " "https://github.com/hukkin/mdformat/issues", ] ) == ( "Error: Could not format\n" '"/home/user/file_name_longer_than_wrap_width--------------------------------------.md".\n' # noqa: E501 "\n" "The formatted Markdown renders to different HTML than the input\n" "Markdown. This is likely a bug in mdformat. Please create an issue\n" "report here: https://github.com/hukkin/mdformat/issues\n" ) def test_version(capsys): with pytest.raises(SystemExit) as exc_info: run(["--version"]) assert exc_info.value.code == 0 captured = capsys.readouterr() assert captured.out.startswith(f"mdformat {mdformat.__version__}") def test_no_wrap(tmp_path): file_path = tmp_path / "test_markdown.md" file_path.write_text( "all\n" "these newlines\n" "except the one in this hardbreak \n" "should be\n" "removed because they are\n" "in the same paragraph\n" "\n" "This however is the next\n" "paragraph. Whitespace should be collapsed\n" " \t here\n" ) assert run([str(file_path), "--wrap=no"]) == 0 assert ( file_path.read_text() == "all these newlines except the one in this hardbreak\\\n" "should be removed because they are in the same paragraph\n" "\n" "This however is the next paragraph. Whitespace should be collapsed here\n" ) def test_wrap(tmp_path): file_path = tmp_path / "test_markdown.md" file_path.write_text( "This\n" "text\n" "should\n" "be wrapped again so that wrap width is whatever is defined below. " "Also whitespace should\t\tbe collapsed. " "Next up a second paragraph:\n" "\n" "This paragraph should also be wrapped. " "Here's some more text to wrap. " "Here's some more text to wrap. " "Here's some more text to wrap. " ) assert run([str(file_path), "--wrap=60"]) == 0 assert ( file_path.read_text() == "This text should be wrapped again so that wrap width is\n" "whatever is defined below. Also whitespace should be\n" "collapsed. Next up a second paragraph:\n" "\n" "This paragraph should also be wrapped. Here's some more text\n" "to wrap. Here's some more text to wrap. Here's some more\n" "text to wrap.\n" ) def test_consecutive_wrap_width_lines(tmp_path): """Test a case where two consecutive lines' width equals wrap width. There was a bug where this would cause an extra newline and split the paragraph, hence the test. """ wrap_width = 20 file_path = tmp_path / "test_markdown.md" text = "A" * wrap_width + "\n" + "A" * wrap_width + "\n" file_path.write_text(text) assert run([str(file_path), f"--wrap={wrap_width}"]) == 0 assert file_path.read_text() == text def test_wrap__hard_break(tmp_path): file_path = tmp_path / "test_markdown.md" file_path.write_text( "This\n" "text\n" "should\n" "be wrapped again\\\n" "so that wrap width is whatever is defined below. " "Also whitespace should\t\tbe collapsed." ) assert run([str(file_path), "--wrap=60"]) == 0 assert ( file_path.read_text() == "This text should be wrapped again\\\n" "so that wrap width is whatever is defined below. Also\n" "whitespace should be collapsed.\n" ) def test_bad_wrap_width(capsys): with pytest.raises(SystemExit) as exc_info: run(["some-path.md", "--wrap=-1"]) assert exc_info.value.code == 2 captured = capsys.readouterr() assert "error: argument --wrap" in captured.err def test_eol__lf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"Oi\r\n") assert run([str(file_path)]) == 0 assert file_path.read_bytes() == b"Oi\n" def test_eol__crlf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"Oi\n") assert run([str(file_path), "--end-of-line=crlf"]) == 0 assert file_path.read_bytes() == b"Oi\r\n" def test_eol__keep_lf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"Oi\n") assert run([str(file_path), "--end-of-line=keep"]) == 0 assert file_path.read_bytes() == b"Oi\n" def test_eol__keep_crlf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"Oi\r\n") assert run([str(file_path), "--end-of-line=keep"]) == 0 assert file_path.read_bytes() == b"Oi\r\n" def test_eol__crlf_stdin(capfd, monkeypatch): monkeypatch.setattr(sys, "stdin", StringIO("Oi\n")) assert run(["-", "--end-of-line=crlf"]) == 0 captured = capfd.readouterr() assert captured.out == "Oi\r\n" def test_eol__check_lf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"lol\r\n") assert run((str(file_path), "--check")) == 1 file_path.write_bytes(b"lol\n") assert run((str(file_path), "--check")) == 0 def test_eol__check_crlf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"lol\n") assert run((str(file_path), "--check", "--end-of-line=crlf")) == 1 file_path.write_bytes(b"lol\r\n") assert run((str(file_path), "--check", "--end-of-line=crlf")) == 0 def test_eol__check_keep_lf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"lol\n") assert run((str(file_path), "--check", "--end-of-line=keep")) == 0 file_path.write_bytes(b"mixed\nEOLs\r") assert run((str(file_path), "--check", "--end-of-line=keep")) == 1 def test_eol__check_keep_crlf(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"lol\r\n") assert run((str(file_path), "--check", "--end-of-line=keep")) == 0 file_path.write_bytes(b"mixed\r\nEOLs\n") assert run((str(file_path), "--check", "--end-of-line=keep")) == 1 def test_cli_no_validate(tmp_path): file_path = tmp_path / "test.md" content = "1. ordered" file_path.write_text(content) with patch("mdformat.renderer._context.get_list_marker_type", return_value="?"): assert run((str(file_path),)) == 1 assert file_path.read_text() == content assert run((str(file_path), "--no-validate")) == 0 assert file_path.read_text() == "1? ordered\n" def test_get_plugin_info_str(): info = get_plugin_info_str( {"mdformat-tables": ("0.1.0", ["tables"])}, {"mdformat-black": ("12.1.0", ["python"])}, ) assert ( info == """\ installed codeformatters: mdformat-black: python installed extensions: mdformat-tables: tables""" ) def test_no_timestamp_modify(tmp_path): file_path = tmp_path / "test.md" file_path.write_bytes(b"lol\n") initial_access_time = 0 initial_mod_time = 0 os.utime(file_path, (initial_access_time, initial_mod_time)) # Assert that modification time does not change when no changes are applied assert run([str(file_path)]) == 0 assert os.path.getmtime(file_path) == initial_mod_time @pytest.mark.skipif( sys.version_info < (3, 13), reason="'exclude' only possible on 3.13+" ) def test_exclude(tmp_path): subdir_path_1 = tmp_path / "folder1" subdir_path_2 = subdir_path_1 / "folder2" file_path_1 = subdir_path_2 / "file1.md" subdir_path_1.mkdir() subdir_path_2.mkdir() file_path_1.write_text(UNFORMATTED_MARKDOWN) cwd = tmp_path with patch("mdformat._cli.Path.cwd", return_value=cwd): for good_pattern in [ "folder1/folder2/file1.md", "**", "**/*.md", "**/file1.md", "folder1/**", ]: assert run([str(file_path_1), "--exclude", good_pattern]) == 0 assert file_path_1.read_text() == UNFORMATTED_MARKDOWN for bad_pattern in [ "**file1.md", "file1.md", "folder1", "*.md", "*", "folder1/*", ]: file_path_1.write_text(UNFORMATTED_MARKDOWN) assert run([str(file_path_1), "--exclude", bad_pattern]) == 0 assert file_path_1.read_text() == FORMATTED_MARKDOWN def test_codeformatters(tmp_path, monkeypatch): monkeypatch.setitem(CODEFORMATTERS, "enabled-lang", lambda code, info: "dumdum") monkeypatch.setitem(CODEFORMATTERS, "disabled-lang", lambda code, info: "dumdum") file_path = tmp_path / "test.md" unformatted = """\ ```disabled-lang hey ``` ```enabled-lang hey ``` """ formatted = """\ ```disabled-lang hey ``` ```enabled-lang dumdum ``` """ file_path.write_text(unformatted) assert run((str(file_path), "--codeformatters", "enabled-lang")) == 0 assert file_path.read_text() == formatted def test_extensions(tmp_path, monkeypatch): ast_plugin_name = "ast-plug" prefix_plugin_name = "prefix-plug" monkeypatch.setitem(PARSER_EXTENSIONS, ast_plugin_name, ASTChangingPlugin) monkeypatch.setitem(PARSER_EXTENSIONS, prefix_plugin_name, PrefixPostprocessPlugin) unformatted = "original text\n" file_path = tmp_path / "test.md" file_path.write_text(unformatted) assert run((str(file_path), "--extensions", "prefix-plug")) == 0 assert file_path.read_text() == "Prefixed!original text\n" file_path.write_text(unformatted) assert run((str(file_path), "--extensions", "ast-plug")) == 0 assert file_path.read_text() == ASTChangingPlugin.TEXT_REPLACEMENT + "\n" file_path.write_text(unformatted) assert ( run((str(file_path), "--extensions", "ast-plug", "--extensions", "prefix-plug")) == 0 ) assert ( file_path.read_text() == "Prefixed!" + ASTChangingPlugin.TEXT_REPLACEMENT + "\n" ) def test_codeformatters__invalid(tmp_path, capsys): file_path = tmp_path / "test.md" file_path.write_text("") assert run((str(file_path), "--codeformatters", "no-exists")) == 1 captured = capsys.readouterr() assert "Error: Invalid code formatter required" in captured.err def test_extensions__invalid(tmp_path, capsys): file_path = tmp_path / "test.md" file_path.write_text("") assert run((str(file_path), "--extensions", "no-exists")) == 1 captured = capsys.readouterr() assert "Error: Invalid extension required" in captured.err def test_no_codeformatters(tmp_path, monkeypatch): monkeypatch.setitem(CODEFORMATTERS, "lang", lambda code, info: "dumdum") file_path = tmp_path / "test.md" original_md = """\ ```lang original code ``` """ file_path.write_text(original_md) assert run((str(file_path), "--no-codeformatters")) == 0 assert file_path.read_text() == original_md def test_no_extensions(tmp_path, monkeypatch): plugin_name = "plug-name" monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name, ASTChangingPlugin) file_path = tmp_path / "test.md" original_md = "original md\n" file_path.write_text(original_md) assert run((str(file_path), "--no-extensions")) == 0 assert file_path.read_text() == original_md mdformat-0.7.22/tests/test_commonmark_spec.py000066400000000000000000000115441475015777400213710ustar00rootroot00000000000000import json from pathlib import Path from _pytest.mark import ParameterSet import pytest import mdformat from mdformat._util import is_md_equal SPECTESTS_PATH = Path(__file__).parent / "data" / "commonmark_spec_v0.30.json" SPECTESTS_CASES = tuple( {"name": str(entry["example"]), "md": entry["markdown"]} for entry in json.loads(SPECTESTS_PATH.read_text(encoding="utf-8")) ) EXTRA_CASES = ( { "name": "titles", "md": "# BIG Title\n" "## smaller\n" "### even smaller\n" "#### 4th level\n" "##### 5th level\n" "###### the smallest (6th level)\n", }, {"name": "emphasis", "md": "*emphasized*\n" "**STRONGLY emphasized**\n"}, { "name": "simple", "md": "# BIG Title\n" "> a quote here\n\n" "Paragraph is here.\n", }, {"name": "strikethrough", "md": "# Testing strikethrough\n" "~~here goes~~\n"}, {"name": "code span", "md": "`print('Hello World!)`\n"}, { "name": "code span: double spaces", "md": "` starts and ends with double spaces `\n", }, {"name": "escape char", "md": "\\*not emphasized\\*\n"}, { "name": "fenced code", "md": "## Here's a code block\n" "```python\n" "print('Hello world!')\n" "```\n", }, {"name": "paragraph line feeds", "md": "first paragraph\n\n" "second one\n"}, {"name": "thematic break", "md": "thematic break\n\n" "---\n" "above\n"}, {"name": "autolink", "md": "\n"}, {"name": "link", "md": '[link](/uri "title")\n' "[link](/uri)\n" "[link]()\n"}, {"name": "image", "md": '![foo](/url "title")\n' "![foo [bar](/url)](/url2)\n"}, {"name": "unordered list", "md": "- item1\n" "- item2\n"}, {"name": "unordered loose list", "md": "- item1\n" "\n" "- item2\n"}, {"name": "ordered list", "md": "1. item1\n" "1. item2\n"}, {"name": "ordered list 2", "md": "10021. item1\n" "10021. item2\n"}, {"name": "ordered list zero", "md": "0. item1\n" "0. item2\n"}, {"name": "whitespace preserve", "md": "- foo\n\n\tbar\n"}, {"name": "debug", "md": "> ```\n> a\n> \n> \n> ```\n"}, {"name": "list indentation", "md": "- foo\n\n\t\tbar\n"}, {"name": "list in quote", "md": "> -"}, {"name": "starts with >", "md": "\\>"}, {"name": "reference link", "md": '[foo][bar]\n\n[bar]: /url "title"\n'}, {"name": "empty file", "md": ""}, {"name": "whitespace only", "md": " \n\n \n \n"}, {"name": "starts with em space", "md": " \n"}, {"name": "starts with space", "md": " \n"}, {"name": "trailing space", "md": "trailing space \n"}, {"name": "soft breaks", "md": "this is\nall one\nparagraph\n"}, {"name": "escape underscore", "md": "# foo _bar_ \\_baz\\_\n"}, { "name": "extend spectest 300", "md": "\\_not emphasized_\n" "1\\) not a list\n" "\\- not a list\n" "\\+ not a list\n", }, {"name": "image link with brackets 1", "md": "![link]()\n"}, {"name": "image link with brackets 2", "md": "![link](foo\\(and\\(bar\\))\n"}, {"name": "image link with brackets 3", "md": "![link](foo\\)\\:)\n"}, {"name": "image link with brackets 4", "md": "![a]()\n"}, {"name": "image link with brackets 5", "md": '![a]( "some title")\n'}, {"name": "escaped thematic break (hyphen)", "md": "\\-\\-\\-\n"}, {"name": "escaped thematic break (underscore v1)", "md": "\\_\\_\\_\n"}, {"name": "escaped thematic break (underscore v2)", "md": " _ _ _ \n"}, {"name": "escaped thematic break (asterisk v1)", "md": "\\*\\*\\*\n"}, {"name": "escaped thematic break (asterisk v2)", "md": " * * * \n"}, {"name": "Hard break in setext heading", "md": "line1\\\nline2\n====\n"}, { "name": "a mix of leading and trailing whitespace", "md": " foo\n" " foo \n" " foo \t\n" " foo \n" "\u2005foo\u2005\n" "foo\n", }, ) ALL_CASES = EXTRA_CASES + SPECTESTS_CASES @pytest.mark.parametrize("wrap", ["keep", "no", 60]) @pytest.mark.parametrize("number", [True, False]) @pytest.mark.parametrize( "entry", ALL_CASES, ids=[ ( c.values[0]["name"] # type: ignore[index] if isinstance(c, ParameterSet) else c["name"] ) for c in ALL_CASES ], ) def test_commonmark_spec(wrap, number, entry): """mdformat.text() against the Commonmark spec. Test that: 1. Markdown AST is the same before and after 1 pass of formatting 2. Markdown after 1st pass and 2nd pass of formatting are equal """ options = {"wrap": wrap, "number": number} md_original = entry["md"] md_new = mdformat.text(md_original, options=options) md_2nd_pass = mdformat.text(md_new, options=options) assert is_md_equal(md_original, md_new, options=options) assert md_new == md_2nd_pass mdformat-0.7.22/tests/test_config_file.py000066400000000000000000000125551475015777400204630ustar00rootroot00000000000000from io import StringIO import sys from unittest import mock import pytest from mdformat._cli import run from tests.utils import FORMATTED_MARKDOWN, UNFORMATTED_MARKDOWN def test_cli_override(tmp_path): config_path = tmp_path / ".mdformat.toml" config_path.write_text("wrap = 'no'\nend_of_line = 'lf'") file_path = tmp_path / "test_markdown.md" file_path.write_text("remove\nthis\nwrap\n") assert run((str(file_path), "--wrap=keep")) == 0 assert file_path.read_text() == "remove\nthis\nwrap\n" assert run((str(file_path),)) == 0 assert file_path.read_text() == "remove this wrap\n" def test_conf_in_parent_dir(tmp_path): config_path = tmp_path / ".mdformat.toml" config_path.write_text("wrap = 'no'") subdir_path = tmp_path / "subdir" subdir_path.mkdir() file_path = subdir_path / "test_markdown.md" file_path.write_text("remove\nthis\nwrap") assert run((str(file_path),)) == 0 assert file_path.read_text() == "remove this wrap\n" def test_invalid_conf_key(tmp_path, capsys): config_path = tmp_path / ".mdformat.toml" config_path.write_text("numberr = true") file_path = tmp_path / "test_markdown.md" file_path.write_text("1. one\n1. two\n1. three") assert run((str(file_path),)) == 1 captured = capsys.readouterr() assert "Invalid key 'numberr'" in captured.err def test_invalid_toml(tmp_path, capsys): config_path = tmp_path / ".mdformat.toml" config_path.write_text("]invalid TOML[") file_path = tmp_path / "test_markdown.md" file_path.write_text("some markdown\n") assert run((str(file_path),)) == 1 captured = capsys.readouterr() assert "Invalid TOML syntax" in captured.err @pytest.mark.parametrize( "conf_key, bad_conf", [ ("wrap", "wrap = -3"), ("end_of_line", "end_of_line = 'lol'"), ("validate", "validate = 'off'"), ("number", "number = 0"), ("exclude", "exclude = '**'"), ("exclude", "exclude = ['1',3]"), ("plugin", "plugin = []"), ("plugin", "plugin.gfm = {}\nplugin.myst = 1"), ("codeformatters", "codeformatters = 'python'"), ("extensions", "extensions = 'gfm'"), ("codeformatters", "codeformatters = ['python', 1]"), ("extensions", "extensions = ['gfm', 1]"), ], ) def test_invalid_conf_value(bad_conf, conf_key, tmp_path, capsys): if conf_key == "exclude" and sys.version_info < (3, 13): pytest.skip("exclude conf only on Python 3.13+") config_path = tmp_path / ".mdformat.toml" config_path.write_text(bad_conf) file_path = tmp_path / "test_markdown.md" file_path.write_text("# Test Markdown") assert run((str(file_path),)) == 1 captured = capsys.readouterr() assert f"Invalid '{conf_key}' value" in captured.err def test_conf_with_stdin(tmp_path, capfd, monkeypatch): config_path = tmp_path / ".mdformat.toml" config_path.write_text("number = true") monkeypatch.setattr(sys, "stdin", StringIO("1. one\n1. two\n1. three")) with mock.patch("mdformat._cli.Path.cwd", return_value=tmp_path): assert run(("-",), cache_toml=False) == 0 captured = capfd.readouterr() assert captured.out == "1. one\n2. two\n3. three\n" @pytest.mark.skipif( sys.version_info >= (3, 13), reason="'exclude' only possible on 3.13+" ) def test_exclude_conf_on_old_python(tmp_path, capsys): config_path = tmp_path / ".mdformat.toml" config_path.write_text("exclude = ['**']") file_path = tmp_path / "test_markdown.md" file_path.write_text("# Test Markdown") assert run((str(file_path),)) == 1 assert "only available on Python 3.13+" in capsys.readouterr().err @pytest.mark.skipif( sys.version_info < (3, 13), reason="'exclude' only possible on 3.13+" ) def test_exclude(tmp_path, capsys): config_path = tmp_path / ".mdformat.toml" config_path.write_text("exclude = ['dir1/*', 'file1.md']") dir1_path = tmp_path / "dir1" file1_path = tmp_path / "file1.md" file2_path = tmp_path / "file2.md" file3_path = tmp_path / dir1_path / "file3.md" dir1_path.mkdir() file1_path.write_text(UNFORMATTED_MARKDOWN) file2_path.write_text(UNFORMATTED_MARKDOWN) file3_path.write_text(UNFORMATTED_MARKDOWN) assert run((str(tmp_path),)) == 0 assert file1_path.read_text() == UNFORMATTED_MARKDOWN assert file2_path.read_text() == FORMATTED_MARKDOWN assert file3_path.read_text() == UNFORMATTED_MARKDOWN @pytest.mark.skipif( sys.version_info < (3, 13), reason="'exclude' only possible on 3.13+" ) def test_empty_exclude(tmp_path, capsys): config_path = tmp_path / ".mdformat.toml" config_path.write_text("exclude = []") file1_path = tmp_path / "file1.md" file1_path.write_text(UNFORMATTED_MARKDOWN) assert run((str(tmp_path),)) == 0 assert file1_path.read_text() == FORMATTED_MARKDOWN def test_conf_no_validate(tmp_path): file_path = tmp_path / "file.md" content = "1. ordered" file_path.write_text(content) with mock.patch( "mdformat.renderer._context.get_list_marker_type", return_value="?", ): assert run((str(file_path),), cache_toml=False) == 1 assert file_path.read_text() == content config_path = tmp_path / ".mdformat.toml" config_path.write_text("validate = false") assert run((str(file_path),), cache_toml=False) == 0 assert file_path.read_text() == "1? ordered\n" mdformat-0.7.22/tests/test_context.py000066400000000000000000000011661475015777400176770ustar00rootroot00000000000000from mdformat.renderer import DEFAULT_RENDERERS from mdformat.renderer._context import RenderContext def render_fake_syntax(node, ctx): return "blaa" def test_with_default_renderer_for(): original_renderers = { "fake_syntax": render_fake_syntax, "fake_syntax_2": render_fake_syntax, } ctx = RenderContext( renderers=original_renderers, postprocessors={}, options={}, env={} ) ctx_2 = ctx.with_default_renderer_for("fake_syntax", "paragraph") assert ctx_2.renderers == { "fake_syntax_2": render_fake_syntax, "paragraph": DEFAULT_RENDERERS["paragraph"], } mdformat-0.7.22/tests/test_for_profiler.py000066400000000000000000000017661475015777400207110ustar00rootroot00000000000000"""Test for profiling. This test can be useful for profiling, as most of the execution time will be spent parsing and rendering instead of managing pytest execution environment. The test simply ensures that the README and Markdown docs in this project are formatted. To get and read profiler results: - `tox -e profile` - `firefox .tox/prof/combined.svg` """ from pathlib import Path from mdformat._cli import run PROJECT_ROOT = Path(__file__).parent.parent # Make a few asserts to ensure this actually is the project root # (a safeguard against refactorings where this file is moved). assert (PROJECT_ROOT / "docs").exists() assert (PROJECT_ROOT / "README.md").exists() assert (PROJECT_ROOT / "src" / "mdformat").exists() def test_for_profiler(): docs_path = PROJECT_ROOT / "docs" readme_path = PROJECT_ROOT / "README.md" assert run([str(docs_path), str(readme_path), "--check"]) == 0 # Also profile --wrap=INT code run([str(docs_path), str(readme_path), "--check", "--wrap", "50"]) mdformat-0.7.22/tests/test_plugins.py000066400000000000000000000311331475015777400176710ustar00rootroot00000000000000import argparse from textwrap import dedent from unittest.mock import patch from markdown_it import MarkdownIt import pytest import mdformat from mdformat._cli import run from mdformat._compat import importlib_metadata from mdformat.plugins import ( _PARSER_EXTENSION_DISTS, CODEFORMATTERS, PARSER_EXTENSIONS, _load_entrypoints, ) from mdformat.renderer import MDRenderer from tests.utils import ( ASTChangingPlugin, JSONFormatterPlugin, PrefixPostprocessPlugin, SuffixPostprocessPlugin, TablePlugin, TextEditorPlugin, ) def test_code_formatter(monkeypatch): def fmt_func(code, info): return "dummy\n" monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func) text = mdformat.text( dedent( """\ ```lang a ``` """ ), codeformatters={"lang"}, ) assert text == dedent( """\ ```lang dummy ``` """ ) def test_code_formatter__empty_str(monkeypatch): def fmt_func(code, info): return "" monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func) text = mdformat.text( dedent( """\ ~~~lang aag gw ~~~ """ ), codeformatters={"lang"}, ) assert text == dedent( """\ ```lang ``` """ ) def test_code_formatter__no_end_newline(monkeypatch): def fmt_func(code, info): return "dummy\ndum" monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func) text = mdformat.text( dedent( """\ ```lang ``` """ ), codeformatters={"lang"}, ) assert text == dedent( """\ ```lang dummy dum ``` """ ) def test_code_formatter__interface(monkeypatch): def fmt_func(code, info): return info + code * 2 monkeypatch.setitem(CODEFORMATTERS, "lang", fmt_func) text = mdformat.text( dedent( """\ ``` lang long multi mul ``` """ ), codeformatters={"lang"}, ) assert text == dedent( """\ ```lang long lang longmulti mul multi mul ``` """ ) def test_single_token_extension(monkeypatch): """Test the front matter plugin, as a single token extension example.""" plugin_name = "text_editor" monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name, TextEditorPlugin) text = mdformat.text( dedent( """\ # Example Heading Example paragraph. """ ), extensions=[plugin_name], ) assert text == dedent( """\ # All text is like this now! All text is like this now! """ ) def test_table(monkeypatch): """Test the table plugin, as a multi-token extension example.""" monkeypatch.setitem(PARSER_EXTENSIONS, "table", TablePlugin) text = mdformat.text( dedent( """\ |a|b| |-|-| |c|d| other text """ ), extensions=["table", "table"], ) assert text == dedent( """\ dummy 21 other text """ ) class ExamplePluginWithCli: """A plugin that adds CLI options.""" @staticmethod def update_mdit(mdit: MarkdownIt): mdit.enable("table") @staticmethod def add_cli_options(parser: argparse.ArgumentParser) -> None: parser.add_argument("--o1", type=str) parser.add_argument("--o2", type=str, default="a") parser.add_argument("--o3", dest="arg_name", type=int) def test_cli_options(monkeypatch, tmp_path): """Test that CLI arguments added by plugins are correctly added to the options dict.""" monkeypatch.setitem(PARSER_EXTENSIONS, "table", ExamplePluginWithCli) file_path = tmp_path / "test_markdown.md" file_path.touch() with patch.object(MDRenderer, "render", return_value="") as mock_render: assert run((str(file_path), "--o1", "other", "--o3", "4")) == 0 (call_,) = mock_render.call_args_list posargs = call_[0] # Options is the second positional arg of MDRender.render opts = posargs[1] assert opts["mdformat"]["o1"] == "other" assert opts["mdformat"]["o2"] == "a" assert opts["mdformat"]["arg_name"] == 4 class ExamplePluginWithGroupedCli: """A plugin that adds CLI options.""" @staticmethod def update_mdit(mdit: MarkdownIt): mdit.enable("table") @staticmethod def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: group.add_argument("--o1", type=str) group.add_argument("--o2", type=str, default="a") group.add_argument("--o3", dest="arg_name", type=int) group.add_argument("--override-toml") group.add_argument("--dont-override-toml", action="store_const", const=True) def test_cli_options_group(monkeypatch, tmp_path): """Test that CLI arguments added by plugins are correctly added to the options dict. Use add_cli_argument_group plugin API. """ monkeypatch.setitem(PARSER_EXTENSIONS, "table", ExamplePluginWithGroupedCli) file_path = tmp_path / "test_markdown.md" conf_path = tmp_path / ".mdformat.toml" file_path.touch() conf_path.write_text( """\ [plugin.table] override_toml = 'failed' toml_only = true dont_override_toml = 'dont override this with None if CLI opt not given' """ ) with patch.object(MDRenderer, "render", return_value="") as mock_render: assert ( run( ( str(file_path), "--o1", "other", "--o3", "4", "--override-toml", "success", ) ) == 0 ) (call_,) = mock_render.call_args_list posargs = call_[0] # Options is the second positional arg of MDRender.render opts = posargs[1] table_opts = opts["mdformat"]["plugin"]["table"] assert table_opts["o1"] == "other" assert table_opts["o2"] == "a" assert table_opts["arg_name"] == 4 assert table_opts["override_toml"] == "success" assert table_opts["toml_only"] is True assert ( table_opts["dont_override_toml"] == "dont override this with None if CLI opt not given" ) def test_cli_options_group__no_toml(monkeypatch, tmp_path): """Test add_cli_argument_group plugin API with configuration only from CLI.""" monkeypatch.setitem(PARSER_EXTENSIONS, "table", ExamplePluginWithGroupedCli) file_path = tmp_path / "test_markdown.md" file_path.touch() with patch.object(MDRenderer, "render", return_value="") as mock_render: assert run((str(file_path), "--o1", "other")) == 0 (call_,) = mock_render.call_args_list posargs = call_[0] # Options is the second positional arg of MDRender.render opts = posargs[1] assert opts["mdformat"]["plugin"]["table"]["o1"] == "other" def test_ast_changing_plugin(monkeypatch, tmp_path): plugin = ASTChangingPlugin() monkeypatch.setitem(PARSER_EXTENSIONS, "ast_changer", plugin) file_path = tmp_path / "test_markdown.md" # Test that the AST changing formatting is applied successfully # under normal operation. file_path.write_text("Some markdown here\n") assert run((str(file_path),)) == 0 assert file_path.read_text() == plugin.TEXT_REPLACEMENT + "\n" # Set the plugin's `CHANGES_AST` flag to False and test that the # equality check triggers, notices the AST breaking changes and a # non-zero error code is returned. plugin.CHANGES_AST = False file_path.write_text("Some markdown here\n") assert run((str(file_path),)) == 1 assert file_path.read_text() == "Some markdown here\n" def test_code_format_warnings__cli(monkeypatch, tmp_path, capsys): monkeypatch.setitem(CODEFORMATTERS, "json", JSONFormatterPlugin.format_json) file_path = tmp_path / "test_markdown.md" file_path.write_text("```json\nthis is invalid json\n```\n") assert run([str(file_path)]) == 0 captured = capsys.readouterr() assert ( captured.err == f"Warning: Failed formatting content of a json code block (line 1 before formatting). Filename: {file_path}\n" # noqa: E501 ) def test_code_format_warnings__api(monkeypatch, caplog): monkeypatch.setitem(CODEFORMATTERS, "json", JSONFormatterPlugin.format_json) assert ( mdformat.text("```json\nthis is invalid json\n```\n", codeformatters=("json",)) == "```json\nthis is invalid json\n```\n" ) assert ( caplog.messages[0] == "Failed formatting content of a json code block (line 1 before formatting)" ) def test_plugin_conflict(monkeypatch, tmp_path, capsys): """Test a warning when plugins try to render same syntax.""" plugin_name_1 = "plug1" plugin_name_2 = "plug2" monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name_1, TextEditorPlugin) monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name_2, ASTChangingPlugin) file_path = tmp_path / "test_markdown.md" file_path.write_text("some markdown here") assert run([str(file_path)]) == 0 captured = capsys.readouterr() assert ( captured.err == 'Warning: Plugin conflict. More than one plugin defined a renderer for "text" syntax.\n' # noqa: E501 ) def test_plugin_versions_in_cli_help(monkeypatch, capsys): monkeypatch.setitem( _PARSER_EXTENSION_DISTS, "table-dist", ("v3.2.1", ["table-ext"]) ) with pytest.raises(SystemExit) as exc_info: run(["--help"]) assert exc_info.value.code == 0 captured = capsys.readouterr() assert "installed extensions:" in captured.out assert "table-dist: table-ext" in captured.out def test_postprocess_plugins(monkeypatch): """Test that postprocessors work collaboratively.""" suffix_plugin_name = "suffixer" prefix_plugin_name = "prefixer" monkeypatch.setitem(PARSER_EXTENSIONS, suffix_plugin_name, SuffixPostprocessPlugin) monkeypatch.setitem(PARSER_EXTENSIONS, prefix_plugin_name, PrefixPostprocessPlugin) text = mdformat.text( dedent( """\ # Example Heading. Example paragraph. """ ), extensions=[suffix_plugin_name, prefix_plugin_name], ) assert text == dedent( """\ # Prefixed!Example Heading.Suffixed! Prefixed!Example paragraph.Suffixed! """ ) def test_load_entrypoints(tmp_path, monkeypatch): """Test the function that loads plugins to constants.""" # Create a minimal .dist-info to create EntryPoints out of dist_info_path = tmp_path / "mdformat_gfm-0.3.6.dist-info" dist_info_path.mkdir() entry_points_path = dist_info_path / "entry_points.txt" metadata_path = dist_info_path / "METADATA" # The modules here will get loaded so use ones we know will always exist # (even though they aren't actual extensions). entry_points_path.write_text( """\ [mdformat.parser_extension] ext1=mdformat.plugins ext2=mdformat.plugins """ ) metadata_path.write_text( """\ Metadata-Version: 2.1 Name: mdformat-gfm Version: 0.3.6 """ ) distro = importlib_metadata.PathDistribution(dist_info_path) entrypoints = distro.entry_points loaded_eps, dist_infos = _load_entrypoints(entrypoints) assert loaded_eps == {"ext1": mdformat.plugins, "ext2": mdformat.plugins} assert dist_infos == {"mdformat-gfm": ("0.3.6", ["ext1", "ext2"])} def test_no_codeformatters__toml(tmp_path, monkeypatch): monkeypatch.setitem(CODEFORMATTERS, "json", JSONFormatterPlugin.format_json) unformatted = """\ ```json {"a": "b"} ``` """ formatted = """\ ```json { "a": "b" } ``` """ file1_path = tmp_path / "file1.md" # Without TOML file1_path.write_text(unformatted) assert run((str(tmp_path),)) == 0 assert file1_path.read_text() == formatted # With TOML file1_path.write_text(unformatted) config_path = tmp_path / ".mdformat.toml" config_path.write_text("codeformatters = []") assert run((str(tmp_path),), cache_toml=False) == 0 assert file1_path.read_text() == unformatted def test_no_extensions__toml(tmp_path, monkeypatch): plugin = ASTChangingPlugin() monkeypatch.setitem(PARSER_EXTENSIONS, "ast_changer", plugin) unformatted = "text\n" formatted = plugin.TEXT_REPLACEMENT + "\n" file1_path = tmp_path / "file1.md" # Without TOML file1_path.write_text(unformatted) assert run((str(tmp_path),)) == 0 assert file1_path.read_text() == formatted # With TOML file1_path.write_text(unformatted) config_path = tmp_path / ".mdformat.toml" config_path.write_text("extensions = []") assert run((str(tmp_path),), cache_toml=False) == 0 assert file1_path.read_text() == unformatted mdformat-0.7.22/tests/test_renderer_util.py000066400000000000000000000012461475015777400210550ustar00rootroot00000000000000import pytest from mdformat.renderer import _util @pytest.mark.parametrize( "in_, indexes, out", [ ("text", [0], ["", "text"]), ("text", [3], ["tex", "t"]), ("text", [4], ["text", ""]), ( "lorem dipsum iksum lopsum", [0, 1, 6, 7], ["", "l", "orem ", "d", "ipsum iksum lopsum"], ), ], ) def test_split_at_indexes(in_, indexes, out): assert _util.split_at_indexes(in_, indexes) == out def test_split_at_indexes__valueerror(): with pytest.raises(ValueError) as exc_info: _util.split_at_indexes("testtext", ()) assert "indexes must not be empty" in str(exc_info.value) mdformat-0.7.22/tests/test_style.py000066400000000000000000000016461475015777400173560ustar00rootroot00000000000000from pathlib import Path from markdown_it.utils import read_fixture_file import pytest import mdformat import mdformat._cli @pytest.mark.parametrize( "fixture_file,options", [ ("default_style.md", []), ("consecutive_numbering.md", ["--number"]), ("wrap_width_50.md", ["--wrap=50"]), ], ) def test_style(fixture_file, options, tmp_path): """Test Markdown renderer renders expected style.""" file_path = tmp_path / "test_markdown.md" cases = read_fixture_file(Path(__file__).parent / "data" / fixture_file) for case in cases: line, title, text, expected = case file_path.write_bytes(text.encode()) assert mdformat._cli.run([str(file_path), *options]) == 0 md_new = file_path.read_bytes().decode() if md_new != expected: print("Formatted (unexpected) Markdown below:") print(md_new) assert md_new == expected mdformat-0.7.22/tests/test_util.py000066400000000000000000000011011475015777400171550ustar00rootroot00000000000000from mdformat._util import is_md_equal def test_is_md_equal(): md1 = """ paragraph ```js console.log() ``` paragr """ md2 = """ paragraph ```js bonsole.l()g ``` paragr""" assert not is_md_equal(md1, md2) assert is_md_equal(md1, md2, codeformatters=("js", "go")) def test_is_md_equal__not(): md1 = """ ```js console.log() ``` paragr ```js console.log() ``` """ md2 = """ ```js bonsole.l()g ``` A different paragraph ```js console.log() ``` """ assert not is_md_equal(md1, md2) assert not is_md_equal(md1, md2, codeformatters=("js",)) mdformat-0.7.22/tests/utils.py000066400000000000000000000047701475015777400163200ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING from mdformat.renderer import RenderContext, RenderTreeNode if TYPE_CHECKING: from markdown_it import MarkdownIt UNFORMATTED_MARKDOWN = "\n\n# A header\n\n" FORMATTED_MARKDOWN = "# A header\n" class JSONFormatterPlugin: """A code formatter plugin that formats JSON.""" @staticmethod def format_json(unformatted: str, _info_str: str) -> str: parsed = json.loads(unformatted) return json.dumps(parsed, indent=2) + "\n" class TextEditorPlugin: """A plugin that makes all text the same.""" @staticmethod def update_mdit(mdit: MarkdownIt): pass def _text_renderer( # type: ignore[misc] tree: RenderTreeNode, context: RenderContext ) -> str: return "All text is like this now!" RENDERERS = {"text": _text_renderer} class TablePlugin: """A plugin that adds table extension to the parser.""" @staticmethod def update_mdit(mdit: MarkdownIt): mdit.enable("table") def _table_renderer( # type: ignore[misc] tree: RenderTreeNode, context: RenderContext ) -> str: return "dummy 21" RENDERERS = {"table": _table_renderer} class ASTChangingPlugin: """A plugin that makes AST breaking formatting changes.""" CHANGES_AST = True TEXT_REPLACEMENT = "Content replaced completely. AST is now broken!" @staticmethod def update_mdit(mdit: MarkdownIt): pass def _text_renderer( # type: ignore[misc] tree: RenderTreeNode, context: RenderContext ) -> str: return ASTChangingPlugin.TEXT_REPLACEMENT RENDERERS = {"text": _text_renderer} class PrefixPostprocessPlugin: """A plugin that postprocesses text, adding a prefix.""" CHANGES_AST = True @staticmethod def update_mdit(mdit: MarkdownIt): pass def _text_postprocess( # type: ignore[misc] text: str, tree: RenderTreeNode, context: RenderContext ) -> str: return "Prefixed!" + text RENDERERS: dict = {} POSTPROCESSORS = {"text": _text_postprocess} class SuffixPostprocessPlugin: """A plugin that postprocesses text, adding a suffix.""" CHANGES_AST = True @staticmethod def update_mdit(mdit: MarkdownIt): pass def _text_postprocess( # type: ignore[misc] text: str, tree: RenderTreeNode, context: RenderContext ) -> str: return text + "Suffixed!" RENDERERS: dict = {} POSTPROCESSORS = {"text": _text_postprocess}