├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── pypi.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── BACKERS.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── README.rst ├── benchmark ├── bench.py └── cases │ ├── atx.txt │ ├── auto_links.txt │ ├── blockcode.txt │ ├── blockhtml.txt │ ├── blockquote.txt │ ├── emphasis.txt │ ├── fenced.txt │ ├── insane_ol.txt │ ├── insane_ul.txt │ ├── normal_ol.txt │ ├── normal_ul.txt │ ├── paragraph.txt │ ├── ref_links.txt │ ├── setext.txt │ └── std_links.txt ├── docs ├── Makefile ├── _static │ ├── dark-icon.svg │ ├── light-icon.svg │ ├── logo-black.svg │ └── logo-white.svg ├── _templates │ └── partials │ │ ├── extra-foot.html │ │ └── globaltoc-above.html ├── advanced.rst ├── api.rst ├── changes.rst ├── cli.rst ├── community.rst ├── conf.py ├── directives.rst ├── guide.rst ├── index.rst ├── plugins.rst ├── renderers.rst └── upgrade.rst ├── pyproject.toml ├── requirements-dev.lock ├── requirements-docs.lock ├── serve-doc.py ├── setup.py ├── src └── mistune │ ├── __init__.py │ ├── __main__.py │ ├── block_parser.py │ ├── core.py │ ├── directives │ ├── __init__.py │ ├── _base.py │ ├── _fenced.py │ ├── _rst.py │ ├── admonition.py │ ├── image.py │ ├── include.py │ └── toc.py │ ├── helpers.py │ ├── inline_parser.py │ ├── list_parser.py │ ├── markdown.py │ ├── plugins │ ├── __init__.py │ ├── abbr.py │ ├── def_list.py │ ├── footnotes.py │ ├── formatting.py │ ├── math.py │ ├── ruby.py │ ├── speedup.py │ ├── spoiler.py │ ├── table.py │ ├── task_lists.py │ └── url.py │ ├── py.typed │ ├── renderers │ ├── __init__.py │ ├── _list.py │ ├── html.py │ ├── markdown.py │ └── rst.py │ ├── toc.py │ └── util.py └── tests ├── __init__.py ├── fixtures ├── __init__.py ├── abbr.txt ├── commonmark.json ├── def_list.txt ├── diff-commonmark.txt ├── fenced_admonition.txt ├── fenced_figure.txt ├── fenced_image.txt ├── fenced_toc.txt ├── fix-commonmark.txt ├── footnotes.txt ├── hook_toc.txt ├── include │ ├── hello.md │ ├── hello.txt │ ├── text.html │ └── text.md ├── insert.txt ├── mark.txt ├── math.txt ├── renderer_markdown.txt ├── renderer_rst.txt ├── rst_admonition.txt ├── rst_toc.txt ├── ruby.txt ├── spoiler.txt ├── strikethrough.txt ├── subscript.txt ├── superscript.txt ├── table.txt ├── task_lists.txt └── url.txt ├── test_commonmark.py ├── test_directives.py ├── test_hooks.py ├── test_misc.py ├── test_plugins.py ├── test_renderers.py └── test_syntax.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | insert_final_newline = true 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [*.py] 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [lepture] 4 | patreon: lepture 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: pypi/mistune 8 | custom: https://lepture.com/donate 9 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | 3 | permissions: 4 | contents: write 5 | id-token: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - "v*" 11 | 12 | jobs: 13 | build: 14 | name: build dist files 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: 3.9 23 | 24 | - name: install build 25 | run: python -m pip install --upgrade build 26 | 27 | - name: build dist 28 | run: python -m build 29 | 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | name: artifacts 33 | path: dist/* 34 | if-no-files-found: error 35 | 36 | publish: 37 | environment: 38 | name: pypi-release 39 | 40 | name: release to pypi 41 | needs: build 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/download-artifact@v4 46 | with: 47 | name: artifacts 48 | path: dist 49 | 50 | - name: Push build artifacts to PyPI 51 | uses: pypa/gh-action-pypi-publish@release/v1 52 | 53 | release: 54 | name: write release note 55 | runs-on: ubuntu-latest 56 | needs: publish 57 | 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 62 | - uses: actions/setup-node@v4 63 | with: 64 | node-version: 18 65 | - run: npx changelogithub --no-group 66 | continue-on-error: true 67 | env: 68 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 69 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'wip-*' 7 | paths-ignore: 8 | - 'docs/**' 9 | pull_request: 10 | branches-ignore: 11 | - 'wip-*' 12 | paths-ignore: 13 | - 'docs/**' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | max-parallel: 6 21 | matrix: 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9"] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Python ${{ matrix.python }} on ${{ matrix.os }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python }} 31 | 32 | - name: Install dependencies 33 | run: pip install -r requirements-dev.lock 34 | 35 | - name: Check types with Python ${{ matrix.python }} on ${{ matrix.os }} 36 | run: | 37 | mypy --strict src 38 | mypy src tests 39 | 40 | - name: Test with Python ${{ matrix.python }} on ${{ matrix.os }} 41 | run: pytest 42 | 43 | - name: Report coverage 44 | run: pytest --cov=mistune --cov-report=xml 45 | 46 | - name: Upload coverage to Codecov 47 | uses: codecov/codecov-action@v4 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | files: ./coverage.xml 51 | flags: unittests 52 | name: GitHub 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | .idea 5 | __pycache__ 6 | bin 7 | build 8 | develop-eggs 9 | dist 10 | eggs 11 | parts 12 | .DS_Store 13 | .installed.cfg 14 | docs/_build 15 | cover/ 16 | .tox 17 | *.bak 18 | *.c 19 | *.so 20 | .venv/ 21 | .python-version 22 | .coverage 23 | htmlcov 24 | .eggs 25 | coverage.xml 26 | demo.py 27 | uv.lock 28 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-20.04" 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: requirements-docs.lock 14 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | # Sponsors and Backers 2 | 3 | Many thanks to these awesome sponsors and backers. 4 | 5 | [Support Me via GitHub Sponsors](https://github.com/sponsors/lepture) 6 | 7 | ## Sponsors 8 | 9 | 10 | 11 | 12 | 14 | 15 |
Mistune is sponsored by Typlog, a blogging and podcast hosting platform, simple yet powerful. Write in Markdown. 13 |
16 | 17 | ## Awesome Backers 18 | 19 | 20 | 21 | 27 | 28 |
22 | 23 | Hyunwoo Park 24 |
25 | Hyunwoo Park 26 |
29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Hsiaoming Yang 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | * Neither the name of the creator nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # include all test files but avoid including .pyc 2 | recursive-include benchmark *.py *.txt 3 | recursive-include tests *.json *.html *.md *.py *.txt 4 | # include documentation sources but not built docs 5 | graft docs 6 | prune docs/_build 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build build-docs 2 | 3 | clean: clean-build clean-pyc 4 | 5 | clean-build: 6 | @rm -fr build/ 7 | @rm -fr dist/ 8 | @rm -fr *.egg-info 9 | @rm -fr .coverage 10 | 11 | 12 | clean-pyc: 13 | @find . -name '*.pyc' -exec rm -f {} + 14 | @find . -name '*.pyo' -exec rm -f {} + 15 | @find . -name '*~' -exec rm -f {} + 16 | @find . -name '__pycache__' -exec rm -fr {} + 17 | 18 | 19 | build-docs: 20 | @sphinx-build docs build/_html -a 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Mistune v3 2 | ========== 3 | 4 | A fast yet powerful Python Markdown parser with renderers and plugins. 5 | 6 | Overview 7 | -------- 8 | 9 | Convert Markdown to HTML with ease: 10 | 11 | .. code-block:: python 12 | 13 | import mistune 14 | mistune.html(your_markdown_text) 15 | 16 | Useful Links 17 | ------------ 18 | 19 | 1. GitHub: https://github.com/lepture/mistune 20 | 2. Docs: https://mistune.lepture.com/ 21 | 22 | License 23 | ------- 24 | 25 | Mistune is licensed under BSD. Please see LICENSE for licensing details. 26 | -------------------------------------------------------------------------------- /benchmark/bench.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | ROOT_DIR = os.path.dirname(__file__) 6 | sys.path.insert(0, os.path.join(ROOT_DIR, "..")) 7 | 8 | 9 | CASES = {} 10 | 11 | 12 | def load_case(filename): 13 | if filename == "readme.txt": 14 | filepath = os.path.join(ROOT_DIR, "../README.md") 15 | else: 16 | filepath = os.path.join(ROOT_DIR, "cases", filename) 17 | with open(filepath, "r") as f: 18 | content = f.read() 19 | 20 | name = filename.replace(".txt", "") 21 | CASES[name] = content 22 | return content 23 | 24 | 25 | def run_case(method, content, count=100): 26 | # ignore first trigger 27 | method(content) 28 | 29 | start = time.time() 30 | 31 | while count > 0: 32 | method(content) 33 | count -= 1 34 | 35 | duration = time.time() - start 36 | return duration * 1000 37 | 38 | 39 | def get_markdown_parsers(): 40 | parsers = {} 41 | 42 | import mistune 43 | from mistune.directives import ( 44 | RSTDirective, 45 | Admonition, 46 | TableOfContents, 47 | Include, 48 | ) 49 | 50 | parsers[f"mistune ({mistune.__version__})"] = mistune.html 51 | parsers[f"mistune (slow)"] = mistune.create_markdown(escape=False) 52 | parsers[f"mistune (fast)"] = mistune.create_markdown(escape=False, plugins=["speedup"]) 53 | parsers["mistune (full)"] = mistune.create_markdown( 54 | escape=False, 55 | plugins=[ 56 | "url", 57 | "abbr", 58 | "ruby", 59 | "strikethrough", 60 | "mark", 61 | "insert", 62 | "subscript", 63 | "superscript", 64 | "footnotes", 65 | "def_list", 66 | "math", 67 | "table", 68 | "task_lists", 69 | RSTDirective( 70 | [ 71 | Admonition(), 72 | TableOfContents(), 73 | Include(), 74 | ] 75 | ), 76 | "speedup", 77 | ], 78 | ) 79 | 80 | try: 81 | import mistune_v1 82 | 83 | parsers[f"mistune ({mistune_v1.__version__})"] = mistune_v1.markdown 84 | except ImportError: 85 | pass 86 | 87 | try: 88 | import markdown 89 | 90 | parsers[f"markdown ({markdown.__version__})"] = markdown.markdown 91 | except ImportError: 92 | pass 93 | 94 | try: 95 | from markdown2 import Markdown, __version__ as m2v 96 | 97 | markdowner = Markdown() 98 | parsers[f"markdown2 ({m2v})"] = markdowner.convert 99 | except ImportError: 100 | pass 101 | 102 | try: 103 | import mistletoe 104 | 105 | parsers[f"mistletoe ({mistletoe.__version__})"] = mistletoe.markdown 106 | except ImportError: 107 | pass 108 | 109 | try: 110 | from markdown_it import MarkdownIt, __version__ as mitv 111 | 112 | md = MarkdownIt() 113 | parsers[f"markdown_it ({mitv})"] = md.render 114 | except ImportError: 115 | pass 116 | 117 | return parsers 118 | 119 | 120 | def benchmarks(cases, count=100): 121 | methods = get_markdown_parsers() 122 | for name in cases: 123 | content = load_case(name + ".txt") 124 | 125 | for md_name in methods: 126 | func = methods[md_name] 127 | duration = run_case(func, content, count) 128 | print(f"{md_name} - {name}: {duration}ms") 129 | 130 | 131 | if __name__ == "__main__": 132 | cases = [ 133 | # block 134 | "atx", 135 | "setext", 136 | "normal_ul", 137 | "insane_ul", 138 | "normal_ol", 139 | "insane_ol", 140 | "blockquote", 141 | "blockhtml", 142 | "fenced", 143 | "paragraph", 144 | # inline 145 | "emphasis", 146 | "auto_links", 147 | "std_links", 148 | "ref_links", 149 | "readme", 150 | ] 151 | if len(sys.argv) > 1: 152 | benchmarks(sys.argv[1:]) 153 | else: 154 | benchmarks(cases) 155 | -------------------------------------------------------------------------------- /benchmark/cases/atx.txt: -------------------------------------------------------------------------------- 1 | # h1 heading 2 | ## h2 heading 3 | ### h3 heading 4 | #### h4 heading 5 | ##### h5 heading 6 | ###### h6 heading 7 | ####### not heading 8 | ###### h6 heading ###### 9 | ##### h5 heading ##### 10 | #### h4 heading #### 11 | ### h3 heading ### 12 | ## h2 heading ## 13 | # h1 heading # 14 | 15 | # 16 | 17 | #not heading 18 | -------------------------------------------------------------------------------- /benchmark/cases/auto_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /benchmark/cases/blockcode.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepture/mistune/e8d0a030ce2dd8e7a034fcf61c5f6a29ff677fdf/benchmark/cases/blockcode.txt -------------------------------------------------------------------------------- /benchmark/cases/blockhtml.txt: -------------------------------------------------------------------------------- 1 |
foo 2 | **not bold** 3 | baz 4 |
5 | 6 |
foo 7 | 8 | **is bold** 9 | baz 10 |
11 | 12 | 13 |
14 | pre tag contains blank lines
15 | 
16 | this is still in pre
17 | 
18 | 19 | 24 | 25 | 30 | 31 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /benchmark/cases/blockquote.txt: -------------------------------------------------------------------------------- 1 | > Block quote content 2 | > 3 | > Block quote paragraph 4 | 5 | > Second block quote 6 | > > Nested block quote 7 | > > > > > > > > > > > > > > > > > > > > > Block quote 8 | -------------------------------------------------------------------------------- /benchmark/cases/emphasis.txt: -------------------------------------------------------------------------------- 1 | case for emphasis: **bold** __bold__ *italic* _italic_, **bold and *italic***, **bold *italic* and __bold__**. ______________foo______________ and *********bar********* **__**__bold__**__** 2 | -------------------------------------------------------------------------------- /benchmark/cases/fenced.txt: -------------------------------------------------------------------------------- 1 | ``` 2 | function foo() { 3 | return 'foo' 4 | } 5 | ``` 6 | 7 | ```js 8 | function foo() { 9 | return 'foo' 10 | } 11 | ``` 12 | 13 | ~~~python 14 | def foo(): 15 | return 'foo' 16 | ~~~ 17 | 18 | ~~~python filename=foo.py 19 | def foo(): 20 | return 'foo' 21 | ~~~ 22 | 23 | 24 | ```` 25 | ``` 26 | function foo() { 27 | return 'foo' 28 | } 29 | ``` 30 | ```` 31 | -------------------------------------------------------------------------------- /benchmark/cases/insane_ol.txt: -------------------------------------------------------------------------------- 1 | 1. List Item 2 | 2. List Item 3 | 3) List Item 4 | 3. List Item 5 | 5) List Item 6 | - List Item 7 | 8 | List Item 9 | 2. List Item 10 | 3. List Item 11 | 4. List Item 12 | 5. List Item 13 | 6. List Item 14 | 7. List Item 15 | 8. List Item 16 | 9. List Item 17 | 10. List Item 18 | 11. List Item 19 | 6) List Item 20 | 21 | 1. New list 22 | 23 | 2. Loose item 24 | 25 | 10) List Item 26 | 27 | List Item 28 | 29 | List Item 30 | 31 | List Item 32 | -------------------------------------------------------------------------------- /benchmark/cases/insane_ul.txt: -------------------------------------------------------------------------------- 1 | - List Item 2 | - List Item 3 | - List Item 4 | - List Item 5 | - List Item 6 | - List Item 7 | - List Item 8 | - List Item 9 | - List Item 10 | - List Item 11 | - List Item 12 | - List Item 13 | - List Item 14 | - List Item 15 | - List Item 16 | - List Item 17 | - List Item 18 | - List Item 19 | - List Item 20 | - List Item 21 | - List Item 22 | - List Item 23 | - Loose List Item 24 | 25 | Loose paragraph 26 | - List Item 27 | 28 | - List Item 29 | - List Item 30 | + List Item 31 | - List Item 32 | - List Item 33 | - List Item 34 | 35 | * List Item 36 | 37 | List Item 38 | 39 | List Item 40 | 41 | List Item 42 | -------------------------------------------------------------------------------- /benchmark/cases/normal_ol.txt: -------------------------------------------------------------------------------- 1 | 1. List Item 2 | 2. List Item 3 | 3) List Item 4 | 3. List Item 5 | 6 | 1) New list 7 | 8 | 2) Loose item 9 | 10 | 10. List Item 11 | 12 | List Item 13 | 14 | List Item 15 | 16 | List Item 17 | -------------------------------------------------------------------------------- /benchmark/cases/normal_ul.txt: -------------------------------------------------------------------------------- 1 | - List Item 2 | - List Item 3 | - List Item 4 | - List Item 5 | - List Item 6 | - List Item 7 | 8 | - List Item 9 | - List Item 10 | + List Item 11 | - List Item 12 | - List Item 13 | - List Item 14 | 15 | * List Item 16 | 17 | List Item 18 | 19 | List Item 20 | 21 | List Item 22 | 23 | * List Item 24 | 25 | List Item 26 | 27 | List Item 28 | 29 | List Item 30 | 31 | * List Item 32 | 33 | List Item 34 | 35 | List Item 36 | 37 | List Item 38 | -------------------------------------------------------------------------------- /benchmark/cases/ref_links.txt: -------------------------------------------------------------------------------- 1 | [f1]: /bar 2 | 3 | [f2]: /bar "baz" 4 | 5 | [f3]: 6 | 7 | [f4]: "baz" 8 | 9 | [foo][f1] [foo][f2] [foo][f3] [foo][f4] 10 | 11 | [f1][] [f2][] [f3][] [f4][] 12 | 13 | [f1] [f2] [f3] [f4] 14 | 15 | ![foo][f1] ![foo][f2] ![foo][f3] ![foo][f4] 16 | 17 | ![f1][] ![f2][] ![f3][] ![f4][] 18 | 19 | ![f1] ![f2] ![f3] ![f4] 20 | -------------------------------------------------------------------------------- /benchmark/cases/setext.txt: -------------------------------------------------------------------------------- 1 | h1 2 | == 3 | 4 | h2 5 | -- 6 | 7 | ---- 8 | 9 | heading 10 | ------- 11 | 12 | h1 13 | == 14 | h1 15 | == 16 | h1 17 | == 18 | h1 19 | h1 20 | h1 21 | h1 22 | h1 23 | h1 24 | == 25 | -------------------------------------------------------------------------------- /benchmark/cases/std_links.txt: -------------------------------------------------------------------------------- 1 | [foo](/bar) [foo](/bar "baz") [foo]() [foo]( "baz") 2 | ![foo](/bar) ![foo](/bar "baz") ![foo]() ![foo]( "baz") 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/dark-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/light-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_templates/partials/extra-foot.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_templates/partials/globaltoc-above.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /docs/advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced Guide 2 | ============== 3 | 4 | 5 | Create plugins 6 | -------------- 7 | 8 | Mistune has many built-in plugins, you can take a look at the source code 9 | in ``mistune/plugins`` to find out how to write a plugin. In this documentation, 10 | I'll guide you with an example, let's take a look at the math plugin 11 | (located at ``mistune/plugins/math.py``): 12 | 13 | .. code-block:: python 14 | 15 | def math(md): 16 | md.block.register('block_math', BLOCK_MATH_PATTERN, parse_block_math, before='list') 17 | md.inline.register('inline_math', INLINE_MATH_PATTERN, parse_inline_math, before='link') 18 | if md.renderer and md.renderer.NAME == 'html': 19 | md.renderer.register('block_math', render_block_math) 20 | md.renderer.register('inline_math', render_inline_math) 21 | 22 | The parameter ``md`` is the instance of :class:`Markdown`. In our example, we have registered 23 | a block level math plugin and an inline level math plugin. 24 | 25 | Block level plugin 26 | ~~~~~~~~~~~~~~~~~~ 27 | 28 | Function ``md.block.register`` will register a block level plugin. In the math example: 29 | 30 | .. code-block:: text 31 | 32 | $$ 33 | \operatorname{ker} f=\{g\in G:f(g)=e_{H}\}{\mbox{.}} 34 | $$ 35 | 36 | This is how a block level math syntax looks like. Our ``BLOCK_MATH_PATTERN`` is: 37 | 38 | .. code-block:: python 39 | 40 | # block level pattern MUST startswith ^ 41 | BLOCK_MATH_PATTERN = r'^ {0,3}\$\$[ \t]*\n(?P.+?)\n\$\$[ \t]*$' 42 | 43 | # regex represents: 44 | BLOCK_MATH_PATTERN = ( 45 | r'^ {0,3}' # line can startswith 0~3 spaces just like other block elements defined in commonmark 46 | r'\$\$' # followed by $$ 47 | r'[ \t]*\n' # this line can contain extra spaces and tabs 48 | r'(?P.+?)' # this is the math content, MUST use named group 49 | r'\n\$\$[ \t]*$' # endswith $$ + extra spaces and tabs 50 | ) 51 | 52 | # if you want to make the math pattern more strictly, it could be like: 53 | BLOCK_MATH_PATTERN = r'^\$\$\n(?P.+?)\n\$\$$' 54 | 55 | Then the block parsing function: 56 | 57 | .. code-block:: python 58 | 59 | def parse_block_math(block, m, state): 60 | text = m.group('math_text') 61 | # use ``state.append_token`` to save parsed block math token 62 | state.append_token({'type': 'block_math', 'raw': text}) 63 | # return the end position of parsed text 64 | # since python doesn't count ``$``, we have to +1 65 | # if the pattern is not ended with `$`, we can't +1 66 | return m.end() + 1 67 | 68 | The ``token`` MUST contain ``type``, others are optional. Here are some examples: 69 | 70 | .. code-block:: python 71 | 72 | {'type': 'thematic_break'} #
73 | {'type': 'paragraph', 'text': text} 74 | {'type': 'block_code', 'raw': code} 75 | {'type': 'heading', 'text': text, 'attrs': {'level': level}} 76 | 77 | - **text**: inline parser will parse text 78 | - **raw**: inline parser WILL NOT parse the content 79 | - **attrs**: extra information saved here, renderer will use attrs 80 | 81 | Inline level plugin 82 | ~~~~~~~~~~~~~~~~~~~ 83 | 84 | Function ``md.inline.register`` will register an inline level plugin. In the math example: 85 | 86 | .. code-block:: text 87 | 88 | function $f$ 89 | 90 | This is how an inline level math syntax looks like. Our ``INLINE_MATH_PATTERN`` is: 91 | 92 | .. code-block:: python 93 | 94 | INLINE_MATH_PATTERN = r'\$(?!\s)(?P.+?)(?!\s)\$' 95 | 96 | # regex represents: 97 | INLINE_MATH_PATTERN = ( 98 | r'\$' # startswith $ 99 | r'(?!\s)' # not whitespace 100 | r'(?P.+?)' # content between `$`, MUST use named group 101 | r'(?!\s)' # not whitespace 102 | r'\$' # endswith $ 103 | ) 104 | 105 | Then the inline parsing function: 106 | 107 | .. code-block:: python 108 | 109 | def parse_inline_math(inline, m, state): 110 | text = m.group('math_text') 111 | # use ``state.append_token`` to save parsed inline math token 112 | state.append_token({'type': 'inline_math', 'raw': text}) 113 | # return the end position of parsed text 114 | return m.end() 115 | 116 | The inline token value looks the same with block token. Available keys: 117 | ``type``, ``raw``, ``text``, ``attrs``. 118 | 119 | Plugin renderers 120 | ~~~~~~~~~~~~~~~~ 121 | 122 | It is suggested to add default HTML renderers for your plugin. A renderer function 123 | looks like: 124 | 125 | .. code-block:: python 126 | 127 | def render_hr(renderer): 128 | # token with only type, like: 129 | # {'type': 'hr'} 130 | return '
' 131 | 132 | def render_math(renderer, text): 133 | # token with type and (text or raw), e.g.: 134 | # {'type': 'block_math', 'raw': 'a^b'} 135 | return '
$$' + text + '$$
' 136 | 137 | def render_link(renderer, text, **attrs): 138 | # token with type, text or raw, and attrs 139 | href = attrs['href'] 140 | return f'{text}' 141 | 142 | If current markdown instance is using HTML renderer, developers can register 143 | the plugin renderer for converting markdown to HTML. 144 | 145 | 146 | Write directives 147 | ---------------- 148 | 149 | Mistune has some built-in directives that have been presented in 150 | the directives part of the documentation. These are defined in the 151 | ``mistune/directives``, you can learn how to write a new directive 152 | by reading the source code in ``mistune/directives/``. 153 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Here are the list of API reference; it might be helpful for developers. 5 | 6 | .. module:: mistune 7 | 8 | Basic 9 | ----- 10 | 11 | .. function:: html(text: str) 12 | 13 | :param text: markdown formatted text 14 | 15 | Turn markdown text into HTML without escaping. For instance:: 16 | 17 | text = '**hello** world' 18 | mistune.html(text) 19 | 20 | # => 21 | '

hello world

' 22 | 23 | .. autofunction:: create_markdown 24 | 25 | Utilities 26 | --------- 27 | 28 | .. autofunction:: escape 29 | 30 | .. autofunction:: escape_url 31 | 32 | .. autofunction:: safe_entity 33 | 34 | .. autofunction:: unikey 35 | 36 | Advanced 37 | -------- 38 | 39 | .. autoclass:: Markdown 40 | 41 | .. autoclass:: BlockState 42 | :inherited-members: 43 | 44 | .. autoclass:: InlineState 45 | :inherited-members: 46 | 47 | .. autoclass:: BlockParser 48 | :inherited-members: register 49 | 50 | .. autoclass:: InlineParser 51 | :inherited-members: register 52 | 53 | Plugins 54 | ------- 55 | 56 | .. module:: mistune.plugins.footnotes 57 | 58 | .. autofunction:: footnotes 59 | 60 | .. module:: mistune.plugins.task_lists 61 | 62 | .. autofunction:: task_lists 63 | 64 | .. module:: mistune.plugins.abbr 65 | 66 | .. autofunction:: abbr 67 | 68 | .. module:: mistune.plugins.def_list 69 | 70 | .. autofunction:: def_list 71 | 72 | .. module:: mistune.plugins.table 73 | 74 | .. autofunction:: table 75 | 76 | .. autofunction:: table_in_quote 77 | 78 | .. autofunction:: table_in_list 79 | 80 | .. module:: mistune.plugins.math 81 | 82 | .. autofunction:: math 83 | 84 | .. autofunction:: math_in_quote 85 | 86 | .. autofunction:: math_in_list 87 | 88 | .. module:: mistune.plugins.ruby 89 | 90 | .. autofunction:: ruby 91 | 92 | .. module:: mistune.plugins.formatting 93 | 94 | .. autofunction:: strikethrough 95 | 96 | .. autofunction:: mark 97 | 98 | .. autofunction:: insert 99 | 100 | .. autofunction:: superscript 101 | 102 | .. autofunction:: subscript 103 | 104 | .. module:: mistune.plugins.spoiler 105 | 106 | .. autofunction:: spoiler 107 | 108 | Renderers 109 | --------- 110 | 111 | .. module:: mistune.renderers.html 112 | 113 | .. autoclass:: HTMLRenderer 114 | 115 | .. module:: mistune.renderers.markdown 116 | 117 | .. autoclass:: MarkdownRenderer 118 | 119 | .. module:: mistune.renderers.rst 120 | 121 | .. autoclass:: RSTRenderer 122 | 123 | TOC hook 124 | -------- 125 | 126 | .. module:: mistune.toc 127 | 128 | .. autofunction:: add_toc_hook 129 | 130 | .. autofunction:: render_toc_ul 131 | 132 | 133 | Directives 134 | ---------- 135 | 136 | .. module:: mistune.directives 137 | 138 | .. autoclass:: RSTDirective 139 | 140 | .. autoclass:: FencedDirective 141 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Here is the full history of mistune v3. 5 | 6 | Version 3.1.2 7 | ------------- 8 | 9 | **Released on Feb 19, 2025** 10 | 11 | * Fix ``footnotes`` plugin for AST renderer 12 | 13 | Version 3.1.1 14 | ------------- 15 | 16 | **Released on Jan 28, 2025** 17 | 18 | * Fix ``render_toc_ul`` method for empty iterable 19 | * Fix ruby plugin's regex pattern and HTML rendering 20 | 21 | Version 3.1.0 22 | ------------- 23 | 24 | **Released on Dec 30, 2024** 25 | 26 | * Fix only HTML-escape URLs when rendering to HTML 27 | * Add block_quote prefix on empty lines too 28 | 29 | Version 3.0.2 30 | ------------- 31 | 32 | **Released on Sep 30, 2023** 33 | 34 | * Fix list parser to avoid RecursionError 35 | 36 | Version 3.0.1 37 | ------------- 38 | 39 | **Released on Jun 10, 2023** 40 | 41 | * Add ``py.typed`` for mypy 42 | * Add ``tests``, ``docs`` for sdist 43 | * Support ``renderer="ast"`` for rendering AST 44 | 45 | Version 3.0.0 46 | ------------- 47 | 48 | **Released on Jun 08, 2023** 49 | 50 | * Do not strip leading unicode spaces like emsp 51 | * Ensure new line at end of the text 52 | 53 | Version 3.0.0rc5 54 | ---------------- 55 | 56 | **Released on Mar 22, 2023** 57 | 58 | * Fix fenced directives 59 | * Fix inline link parser 60 | * Fix block math plugin for multiple lines 61 | * Fix empty list item for markdown renderer 62 | 63 | Version 3.0.0rc4 64 | ---------------- 65 | 66 | **Released on Nov 30, 2022** 67 | 68 | * Fix plugin footnotes when there is no newline at the end 69 | * Move safe HTML entities to HTMLRenderer 70 | * Redesign directives parsing 71 | * Add Image and Figure directive 72 | 73 | Version 3.0.0rc3 74 | ---------------- 75 | 76 | **Released on Nov 25, 2022** 77 | 78 | * Render inline math with ``\(`` and ``\)`` 79 | * Added ``RSTRenderer``, and ``MarkdownRenderer`` 80 | * Fix ``toc_hook`` method 81 | * **Breaking change**, rename ``RstDirective`` to ``RSTDirective`` 82 | 83 | Version 3.0.0rc2 84 | ---------------- 85 | 86 | **Released on Nov 6, 2022** 87 | 88 | * Add **spoiler** plugin 89 | * Add ``collapse`` option for ``TableOfContents`` directive 90 | * **Breaking change** on directive design, added fenced directive 91 | 92 | Version 3.0.0rc1 93 | ---------------- 94 | 95 | **Released on Sep 26, 2022** 96 | 97 | * Add **superscript** plugin 98 | 99 | Version 3.0.0a3 100 | --------------- 101 | 102 | **Released on Jul 14, 2022** 103 | 104 | * Fix ruby plugin 105 | * Change toc parameter ``depth`` to ``level`` 106 | 107 | Version 3.0.0a2 108 | --------------- 109 | 110 | **Released on Jul 13, 2022** 111 | 112 | * Escape block code in HTMLRenderer 113 | * Fix parsing links 114 | 115 | Version 3.0.0a1 116 | --------------- 117 | 118 | **Released on Jul 12, 2022** 119 | 120 | This is the first release of v3. Features included: 121 | 122 | * redesigned mistune 123 | * plugins 124 | * directives 125 | -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | Command line tools 2 | ================== 3 | 4 | .. meta:: 5 | :description: How to use the command line tools of Mistune 6 | to convert Markdown to HTML, RST, and etc. 7 | 8 | A command line tool to convert markdown content into HTML, learn 9 | about the options of the command line tool:: 10 | 11 | $ python -m mistune -h 12 | 13 | Mistune, a sane and fast python markdown parser. 14 | 15 | Here are some use cases of the command line tool: 16 | 17 | $ python -m mistune -m "Hi **Markdown**" 18 |

Hi Markdown

19 | 20 | $ python -m mistune -f README.md 21 |

... 22 | 23 | $ cat README.md | python -m mistune 24 |

... 25 | 26 | optional arguments: 27 | -h, --help show this help message and exit 28 | -m MESSAGE, --message MESSAGE 29 | the markdown message to convert 30 | -f FILE, --file FILE the markdown file to convert 31 | -p NAME [NAME ...], --plugin NAME [NAME ...] 32 | specifiy a plugin to use 33 | --escape turn on escape option 34 | --hardwrap turn on hardwrap option 35 | -o OUTPUT, --output OUTPUT 36 | write the rendered result into file 37 | -r RENDERER, --renderer RENDERER 38 | specify the output renderer 39 | --version show program's version number and exit 40 | 41 | Convert Markdown to HTML 42 | ------------------------ 43 | 44 | By default, the command line tool of mistune will convert markdown text 45 | to HTML text:: 46 | 47 | $ python -m mistune -f README.md 48 | 49 | Convert Markdown to RestructuredText 50 | ------------------------------------ 51 | 52 | Mistune has a built-in RestructuredText formatter, specify the renderer 53 | with ``-r rst``:: 54 | 55 | $ python -m mistune -f README.md -r rst 56 | 57 | Reformat Markdown 58 | ----------------- 59 | 60 | You can reformat the markdown file with a markdown renderer:: 61 | 62 | $ python -m mistune -f README.md -r markdown -o README.md 63 | 64 | This command will reformat the text in ``README.md``. 65 | 66 | Unix PIPE 67 | --------- 68 | 69 | The command line tool supports unix PIPE. For instance:: 70 | 71 | $ echo "foo **bar**" | python -m mistune 72 | -------------------------------------------------------------------------------- /docs/community.rst: -------------------------------------------------------------------------------- 1 | Community Extensions 2 | ==================== 3 | 4 | Here is the list of plugins and directives created by the community. 5 | 6 | * `mistune-telegram `_ - Plugin mistune for converting Markdown into Telegram format. 7 | * `mistune-json `_ - Plugin for converting Markdown into HTML-based JSON objects. 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | 3 | project = "Mistune" 4 | copyright = "2019, Hsiaoming Yang" 5 | author = "Hsiaoming Yang" 6 | 7 | master_doc = "index" 8 | 9 | # The full version, including alpha/beta/rc tags 10 | version = mistune.__version__ 11 | release = version 12 | 13 | 14 | # -- General configuration --------------------------------------------------- 15 | 16 | # Add any Sphinx extension module names here, as strings. They can be 17 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 18 | # ones. 19 | extensions = [ 20 | "sphinx.ext.autodoc", 21 | "sphinx.ext.intersphinx", 22 | ] 23 | 24 | intersphinx_mapping = { 25 | "python": ("https://docs.python.org/3", None), 26 | } 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ["_templates"] 30 | 31 | # List of patterns, relative to source directory, that match files and 32 | # directories to ignore when looking for source files. 33 | # This pattern also affects html_static_path and html_extra_path. 34 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 35 | 36 | 37 | # -- Options for HTML output ------------------------------------------------- 38 | 39 | # The theme to use for HTML and HTML Help pages. See the documentation for 40 | # a list of builtin themes. 41 | # 42 | html_theme = "shibuya" 43 | html_context = { 44 | "source_type": "github", 45 | "source_user": "lepture", 46 | "source_repo": "mistune", 47 | } 48 | html_theme_options = { 49 | "accent_color": "indigo", 50 | "light_logo": "_static/logo-black.svg", 51 | "dark_logo": "_static/logo-white.svg", 52 | "twitter_site": "lepture", 53 | "twitter_creator": "lepture", 54 | "twitter_url": "https://twitter.com/lepture", 55 | "github_url": "https://github.com/lepture/mistune", 56 | "nav_links": [ 57 | { 58 | "title": "Projects", 59 | "children": [ 60 | {"title": "Authlib", "url": "https://authlib.org/", "summary": "OAuth, JOSE, OpenID, etc"}, 61 | { 62 | "title": "Shibuya", 63 | "url": "https://shibuya.lepture.com/", 64 | "summary": "A pretty Sphinx theme", 65 | }, 66 | ], 67 | }, 68 | {"title": "Sponsor me", "url": "https://github.com/sponsors/lepture"}, 69 | ], 70 | } 71 | 72 | # Add any paths that contain custom static files (such as style sheets) here, 73 | # relative to this directory. They are copied after the builtin static files, 74 | # so a file named "default.css" will overwrite the builtin "default.css". 75 | html_static_path = ["_static"] 76 | 77 | html_copy_source = False 78 | html_show_sourcelink = False 79 | 80 | html_favicon = "_static/light-icon.svg" 81 | -------------------------------------------------------------------------------- /docs/directives.rst: -------------------------------------------------------------------------------- 1 | .. _directives: 2 | 3 | Directives 4 | ========== 5 | 6 | A directive is a generic block of explicit markup that is powerful 7 | and extensible. In mistune v3, there are 2 styles of directives for 8 | now: 9 | 10 | 1. reStructuredText style 11 | 2. fenced style 12 | 13 | .. versionchanged:: 3.0 14 | 15 | Fenced style directive is added in 3.0. Because v3 has multiple 16 | styles of directives, developers can not add each directive into 17 | ``plugins`` parameter of ``mistune.create_markdown`` directly. 18 | Instead, each directive should be wrapped by:: 19 | 20 | import mistune 21 | from mistune.directives import FencedDirective, RSTDirective 22 | from mistune.directives import Admonition, TableOfContents 23 | 24 | markdown = mistune.create_markdown(plugins=[ 25 | 'math', 26 | 'footnotes', 27 | # ... 28 | FencedDirective([ 29 | Admonition(), 30 | TableOfContents(), 31 | ]), 32 | ]) 33 | 34 | markdown = mistune.create_markdown(plugins=[ 35 | 'math', 36 | 'footnotes', 37 | # ... 38 | RSTDirective([ 39 | Admonition(), 40 | TableOfContents(), 41 | ]), 42 | ]) 43 | 44 | A **reStructuredText** style of directive is inspired by reStructuredText_, 45 | and the syntax looks like: 46 | 47 | .. code-block:: text 48 | 49 | .. directive-type:: title 50 | :option-key: option value 51 | :option-key: option value 52 | 53 | content text here 54 | 55 | 56 | A **fenced** style of directive looks like a fenced code block, it is 57 | inspired by `markdown-it-docutils`_. The syntax looks like: 58 | 59 | .. code-block:: text 60 | 61 | ```{directive-type} title 62 | :option-key: option value 63 | :option-key: option value 64 | 65 | content text here 66 | ``` 67 | 68 | .. _reStructuredText: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#directives 69 | 70 | .. _`markdown-it-docutils`: https://executablebooks.github.io/markdown-it-docutils/ 71 | 72 | 73 | Developers can choose the directive style in their own favor. 74 | 75 | Admonitions 76 | ----------- 77 | 78 | The reStructuredText style syntax: 79 | 80 | .. code-block:: text 81 | 82 | .. warning:: 83 | 84 | You are looking at the **dev** documentation. Check out our 85 | [stable](/stable/) documentation instead. 86 | 87 | The fenced style syntax: 88 | 89 | .. code-block:: text 90 | 91 | ```{warning} 92 | You are looking at the **dev** documentation. Check out our 93 | [stable](/stable/) documentation instead. 94 | ``` 95 | 96 | Admonitions contains a group of ``directive-name``: 97 | 98 | .. code-block:: text 99 | 100 | attention caution danger error 101 | hint important note tip warning 102 | 103 | To enable admonitions:: 104 | 105 | import mistune 106 | from mistune.directives import Admonition 107 | 108 | markdown = mistune.create_markdown( 109 | plugins=[ 110 | ... 111 | RSTDirective([Admonition()]), 112 | # FencedDirective([Admonition()]), 113 | ] 114 | ) 115 | 116 | 117 | Table of Contents 118 | ----------------- 119 | 120 | .. code-block:: text 121 | 122 | .. toc:: Table of Contents 123 | :max-level: 3 124 | 125 | TOC plugin is based on directive. It can add a table of contents section in 126 | the documentation. Let's take an example: 127 | 128 | .. code-block:: text 129 | 130 | Here is the first paragraph, and we put TOC below. 131 | 132 | .. toc:: 133 | 134 | # H1 title 135 | 136 | ## H2 title 137 | 138 | # H1 title 139 | 140 | The rendered HTML will show a TOC at the ``.. toc::`` position. To enable 141 | TOC plugin:: 142 | 143 | import mistune 144 | from mistune.directives import RSTDirective, TableOfContents 145 | 146 | markdown = mistune.create_markdown( 147 | plugins=[ 148 | # ... 149 | RSTDirective([TableOfContents()]), 150 | ] 151 | ) 152 | 153 | Include 154 | ------- 155 | 156 | .. code-block:: text 157 | 158 | .. include:: hello.md 159 | 160 | ``include`` is a powerful plugin for documentation generator. With this 161 | plugin, we can embed contents from other files. 162 | 163 | 164 | Image 165 | ----- 166 | 167 | .. code-block:: text 168 | 169 | ```{image} https://domain/path.png 170 | :alt: alt text 171 | :width: 800 172 | :height: 500 173 | ``` 174 | 175 | Figure 176 | ------ 177 | 178 | .. code-block:: text 179 | 180 | ```{figure} https://domain/path.png 181 | :alt: alt text 182 | :width: 800 183 | :height: 500 184 | ``` 185 | -------------------------------------------------------------------------------- /docs/guide.rst: -------------------------------------------------------------------------------- 1 | How to Use Mistune 2 | ================== 3 | 4 | Mistune is super easy to use. Here is how you can convert Markdown formatted 5 | text into HTML:: 6 | 7 | import mistune 8 | 9 | mistune.html(YOUR_MARKDOWN_TEXT) 10 | 11 | The ``.html()`` methods has enabled all the features you might want 12 | by default: 13 | 14 | * No escape of HTML tags 15 | * With **strikethrough** plugin 16 | * With **table** plugin 17 | * With **footnote** plugin 18 | 19 | 20 | Customize Mistune 21 | ----------------- 22 | 23 | Mistune provides a function to create Markdown instance easily:: 24 | 25 | import mistune 26 | 27 | markdown = mistune.create_markdown() 28 | 29 | This method will create a "escaped" Markdown instance without any plugins, 30 | which means:: 31 | 32 | markdown('

hello
') 33 | # ==> 34 | '

<div>hello</div>

' 35 | 36 | Non escaped version:: 37 | 38 | markdown = mistune.create_markdown(escape=False) 39 | markdown('
hello
') 40 | # ==> 41 | '
hello
' 42 | 43 | Adding plugins:: 44 | 45 | markdown = mistune.create_markdown() 46 | markdown('~~s~~') 47 | # ==> 48 | '

~~s~~

' 49 | 50 | markdown = mistune.create_markdown(plugins=['strikethrough']) 51 | markdown('~~s~~') 52 | # ==> 53 | '

s

' 54 | 55 | Find out what plugins mistune has built-in in :ref:`plugins` sections. 56 | 57 | 58 | Customize Renderer 59 | ------------------ 60 | 61 | Mistune supports renderer feature which enables developers to customize 62 | the output. For instance, to add code syntax highlight:: 63 | 64 | import mistune 65 | from pygments import highlight 66 | from pygments.lexers import get_lexer_by_name 67 | from pygments.formatters import html 68 | 69 | 70 | class HighlightRenderer(mistune.HTMLRenderer): 71 | def block_code(self, code, info=None): 72 | if info: 73 | lexer = get_lexer_by_name(info, stripall=True) 74 | formatter = html.HtmlFormatter() 75 | return highlight(code, lexer, formatter) 76 | return '
' + mistune.escape(code) + '
' 77 | 78 | markdown = mistune.create_markdown(renderer=HighlightRenderer()) 79 | 80 | print(markdown('```python\nassert 1 == 1\n```')) 81 | 82 | In this way, we can use Pygments to highlight the fenced code. Learn more 83 | at :ref:`renderers`. 84 | 85 | 86 | Abstract syntax tree 87 | -------------------- 88 | 89 | Mistune can produce AST by default without any renderer:: 90 | 91 | markdown = mistune.create_markdown(renderer=None) 92 | 93 | This ``markdown`` function will generate a list of tokens instead of HTML:: 94 | 95 | text = 'hello **world**' 96 | markdown(text) 97 | # ==> 98 | [ 99 | { 100 | 'type': 'paragraph', 101 | 'children': [ 102 | {'type': 'text', 'raw': 'hello '}, 103 | {'type': 'strong', 'children': [{'type': 'text', 'raw': 'world'}]} 104 | ] 105 | } 106 | ] 107 | 108 | It is also possible to pass ``renderer='ast'`` to create the markdown instance:: 109 | 110 | markdown = mistune.create_markdown(renderer='ast') 111 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mistune documentation master file, created by 2 | sphinx-quickstart on Wed Sep 18 13:29:37 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Mistune: Python Markdown Parser 7 | =============================== 8 | 9 | Release v\ |version|. 10 | 11 | A fast yet powerful Python Markdown parser with renderers and plugins, 12 | compatible with sane CommonMark rules. 13 | 14 | Using old Mistune? Checkout docs: 15 | 16 | - https://mistune.lepture.com/en/v0.8.4/ 17 | - https://mistune.lepture.com/en/v2/ 18 | 19 | Installation 20 | ------------ 21 | 22 | Installing Mistune is quite easy with `pip `_. 23 | 24 | .. parsed-literal:: 25 | 26 | $ pip install mistune==\ |version| 27 | 28 | Mistune has no external dependencies. 29 | 30 | 31 | User Guide 32 | ---------- 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | 37 | guide 38 | cli 39 | renderers 40 | plugins 41 | directives 42 | advanced 43 | api 44 | upgrade 45 | community 46 | changes 47 | -------------------------------------------------------------------------------- /docs/upgrade.rst: -------------------------------------------------------------------------------- 1 | Upgrade Guide 2 | ============= 3 | 4 | 5 | Upgrade from v2 to v3 6 | --------------------- 7 | 8 | HTMLRenderer 9 | ~~~~~~~~~~~~ 10 | 11 | When customizing renderers, these methods' parameters are changed: 12 | 13 | .. code-block:: diff 14 | 15 | - link(self, link, text=None, title=None) 16 | + link(self, text, url, title=None) 17 | 18 | - image(self, src, alt="", title=None) 19 | + image(self, text, url, title=None) 20 | 21 | - heading(self, text, level) 22 | + heading(self, text, level, **attrs) 23 | 24 | - list(self, text, ordered, level, start=None) 25 | + list(self, text, ordered, **attrs) 26 | 27 | - list_item(self, text, level) 28 | + list_item(self, text) 29 | 30 | - table_cell(self, text, align=None, is_head=False) 31 | + table_cell(self, text, align=None, head=False) 32 | 33 | For plugins: 34 | 35 | .. code-block:: diff 36 | 37 | - abbr(self, key, definition) 38 | + abbr(self, text: str, title: str) 39 | 40 | - task_list_item(self, text: str, level: int, checked: bool) 41 | + task_list_item(self, text: str, checked: bool) 42 | 43 | AstRenderer 44 | ~~~~~~~~~~~ 45 | 46 | There is no ``AstRenderer`` in v3, just pass ``None`` or ``'ast'`` to ``create_markdown``:: 47 | 48 | import mistune 49 | 50 | md = mistune.create_markdown(renderer='ast') # or render=None 51 | md('...markdown text...') 52 | 53 | Plugins 54 | ~~~~~~~ 55 | 56 | Please check the advanced guide and built-in plugins source code to find 57 | out how to write a mistune plugin. 58 | 59 | Directives 60 | ~~~~~~~~~~ 61 | 62 | Find out all the details in :ref:`directives`. In v3, there is one more 63 | style of directive -- fenced style directive. 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mistune" 3 | description = "A sane and fast Markdown parser with useful plugins and renderers" 4 | authors = [{name = "Hsiaoming Yang", email="me@lepture.com"}] 5 | dependencies = [ 6 | "typing-extensions; python_version<'3.11'", 7 | ] 8 | license = {text = "BSD-3-Clause"} 9 | dynamic = ["version"] 10 | requires-python = ">=3.8" 11 | readme = "README.rst" 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Environment :: Console", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python :: Implementation :: CPython", 28 | "Programming Language :: Python :: Implementation :: PyPy", 29 | "Topic :: Text Processing :: Markup", 30 | ] 31 | 32 | [project.urls] 33 | Documentation = "https://mistune.lepture.com/" 34 | Source = "https://github.com/lepture/mistune" 35 | Donate = "https://github.com/sponsors/lepture" 36 | 37 | [build-system] 38 | requires = ["setuptools"] 39 | build-backend = "setuptools.build_meta" 40 | 41 | [dependency-groups] 42 | dev = [ 43 | "mypy", 44 | "pytest", 45 | "pytest-cov", 46 | "ruff", 47 | ] 48 | docs = [ 49 | "shibuya", 50 | "sphinx", 51 | "sphinx-copybutton", 52 | "sphinx-design", 53 | ] 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "mistune.__version__"} 57 | 58 | [tool.setuptools.packages.find] 59 | where = ["src"] 60 | 61 | [tool.setuptools.package-data] 62 | mistune = ["py.typed"] 63 | 64 | [tool.ruff] 65 | line-length = 120 66 | 67 | [tool.pytest.ini_options] 68 | pythonpath = ["src", "."] 69 | testpaths = ["tests"] 70 | filterwarnings = ["error"] 71 | 72 | [tool.coverage.run] 73 | branch = true 74 | source = ["mistune"] 75 | 76 | [tool.coverage.paths] 77 | source = ["src"] 78 | 79 | [tool.coverage.report] 80 | exclude_lines = [ 81 | "pragma: no cover", 82 | "def __repr__", 83 | "raise NotImplementedError", 84 | "@(abc\\.)?abstractmethod", 85 | ] 86 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-hashes -o requirements-dev.lock 3 | -e . 4 | colorama==0.4.6 ; sys_platform == 'win32' 5 | coverage==7.6.1 ; python_full_version < '3.9' 6 | coverage==7.7.0 ; python_full_version >= '3.9' 7 | exceptiongroup==1.2.2 ; python_full_version < '3.11' 8 | iniconfig==2.0.0 9 | mypy==1.14.1 ; python_full_version < '3.9' 10 | mypy==1.15.0 ; python_full_version >= '3.9' 11 | mypy-extensions==1.0.0 12 | packaging==24.2 13 | pluggy==1.5.0 14 | pytest==8.3.5 15 | pytest-cov==5.0.0 ; python_full_version < '3.9' 16 | pytest-cov==6.0.0 ; python_full_version >= '3.9' 17 | ruff==0.11.0 18 | tomli==2.2.1 ; python_full_version <= '3.11' 19 | typing-extensions==4.12.2 20 | -------------------------------------------------------------------------------- /requirements-docs.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-hashes --group docs -o requirements-docs.lock 3 | -e . 4 | alabaster==0.7.13 ; python_full_version < '3.9' 5 | alabaster==0.7.16 ; python_full_version == '3.9.*' 6 | alabaster==1.0.0 ; python_full_version >= '3.10' 7 | babel==2.17.0 8 | certifi==2025.1.31 9 | charset-normalizer==3.4.1 10 | colorama==0.4.6 ; sys_platform == 'win32' 11 | docutils==0.20.1 ; python_full_version < '3.9' 12 | docutils==0.21.2 ; python_full_version >= '3.9' 13 | exceptiongroup==1.2.2 ; python_full_version < '3.11' 14 | idna==3.10 15 | imagesize==1.4.1 16 | importlib-metadata==8.5.0 ; python_full_version < '3.9' 17 | importlib-metadata==8.6.1 ; python_full_version == '3.9.*' 18 | iniconfig==2.0.0 19 | jinja2==3.1.6 20 | markupsafe==2.1.5 ; python_full_version < '3.9' 21 | markupsafe==3.0.2 ; python_full_version >= '3.9' 22 | mypy==1.14.1 ; python_full_version < '3.9' 23 | mypy==1.15.0 ; python_full_version >= '3.9' 24 | mypy-extensions==1.0.0 25 | packaging==24.2 26 | pluggy==1.5.0 27 | pygments==2.19.1 28 | pytest==8.3.5 29 | pytz==2025.1 ; python_full_version < '3.9' 30 | requests==2.32.3 31 | roman-numerals-py==3.1.0 ; python_full_version >= '3.11' 32 | ruff==0.11.0 33 | shibuya==2025.2.28 34 | snowballstemmer==2.2.0 35 | sphinx==7.1.2 ; python_full_version < '3.9' 36 | sphinx==7.4.7 ; python_full_version == '3.9.*' 37 | sphinx==8.1.3 ; python_full_version == '3.10.*' 38 | sphinx==8.2.3 ; python_full_version >= '3.11' 39 | sphinx-copybutton==0.5.2 40 | sphinx-design==0.5.0 ; python_full_version < '3.9' 41 | sphinx-design==0.6.1 ; python_full_version >= '3.9' 42 | sphinxcontrib-applehelp==1.0.4 ; python_full_version < '3.9' 43 | sphinxcontrib-applehelp==2.0.0 ; python_full_version >= '3.9' 44 | sphinxcontrib-devhelp==1.0.2 ; python_full_version < '3.9' 45 | sphinxcontrib-devhelp==2.0.0 ; python_full_version >= '3.9' 46 | sphinxcontrib-htmlhelp==2.0.1 ; python_full_version < '3.9' 47 | sphinxcontrib-htmlhelp==2.1.0 ; python_full_version >= '3.9' 48 | sphinxcontrib-jsmath==1.0.1 49 | sphinxcontrib-qthelp==1.0.3 ; python_full_version < '3.9' 50 | sphinxcontrib-qthelp==2.0.0 ; python_full_version >= '3.9' 51 | sphinxcontrib-serializinghtml==1.1.5 ; python_full_version < '3.9' 52 | sphinxcontrib-serializinghtml==2.0.0 ; python_full_version >= '3.9' 53 | tomli==2.2.1 ; python_full_version < '3.11' 54 | typing-extensions==4.12.2 55 | urllib3==2.2.3 ; python_full_version < '3.9' 56 | urllib3==2.3.0 ; python_full_version >= '3.9' 57 | zipp==3.20.2 ; python_full_version < '3.9' 58 | zipp==3.21.0 ; python_full_version == '3.9.*' 59 | -------------------------------------------------------------------------------- /serve-doc.py: -------------------------------------------------------------------------------- 1 | from livereload import Server, shell 2 | 3 | app = Server() 4 | app.watch("docs", shell("make build-docs"), delay=2) 5 | app.serve(root="build/_html/") 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name="mistune") 4 | -------------------------------------------------------------------------------- /src/mistune/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | mistune 3 | ~~~~~~~ 4 | 5 | A fast yet powerful Python Markdown parser with renderers and 6 | plugins, compatible with sane CommonMark rules. 7 | 8 | Documentation: https://mistune.lepture.com/ 9 | """ 10 | 11 | from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, Literal 12 | from .block_parser import BlockParser 13 | from .core import BaseRenderer, BlockState, InlineState 14 | from .inline_parser import InlineParser 15 | from .markdown import Markdown 16 | from .plugins import Plugin, PluginRef, import_plugin 17 | from .renderers.html import HTMLRenderer 18 | from .util import escape, escape_url, safe_entity, unikey 19 | 20 | RendererRef = Union[Literal["html", "ast"], BaseRenderer] 21 | 22 | 23 | def create_markdown( 24 | escape: bool = True, 25 | hard_wrap: bool = False, 26 | renderer: Optional[RendererRef] = "html", 27 | plugins: Optional[Iterable[PluginRef]] = None, 28 | ) -> Markdown: 29 | """Create a Markdown instance based on the given condition. 30 | 31 | :param escape: Boolean. If using html renderer, escape html. 32 | :param hard_wrap: Boolean. Break every new line into ``
``. 33 | :param renderer: renderer instance, default is HTMLRenderer. 34 | :param plugins: List of plugins. 35 | 36 | This method is used when you want to re-use a Markdown instance:: 37 | 38 | markdown = create_markdown( 39 | escape=False, 40 | hard_wrap=True, 41 | ) 42 | # re-use markdown function 43 | markdown('.... your text ...') 44 | """ 45 | if renderer == "ast": 46 | # explicit and more similar to 2.x's API 47 | renderer = None 48 | elif renderer == "html": 49 | renderer = HTMLRenderer(escape=escape) 50 | 51 | inline = InlineParser(hard_wrap=hard_wrap) 52 | real_plugins: Optional[Iterable[Plugin]] = None 53 | if plugins is not None: 54 | real_plugins = [import_plugin(n) for n in plugins] 55 | return Markdown(renderer=renderer, inline=inline, plugins=real_plugins) 56 | 57 | 58 | html: Markdown = create_markdown(escape=False, plugins=["strikethrough", "footnotes", "table", "speedup"]) 59 | 60 | 61 | __cached_parsers: Dict[Tuple[bool, Optional[RendererRef], Optional[Iterable[Any]]], Markdown] = {} 62 | 63 | 64 | def markdown( 65 | text: str, 66 | escape: bool = True, 67 | renderer: Optional[RendererRef] = "html", 68 | plugins: Optional[Iterable[Any]] = None, 69 | ) -> Union[str, List[Dict[str, Any]]]: 70 | if renderer == "ast": 71 | # explicit and more similar to 2.x's API 72 | renderer = None 73 | key = (escape, renderer, plugins) 74 | if key in __cached_parsers: 75 | return __cached_parsers[key](text) 76 | 77 | md = create_markdown(escape=escape, renderer=renderer, plugins=plugins) 78 | # improve the speed for markdown parser creation 79 | __cached_parsers[key] = md 80 | return md(text) 81 | 82 | 83 | __all__ = [ 84 | "Markdown", 85 | "HTMLRenderer", 86 | "BlockParser", 87 | "BlockState", 88 | "BaseRenderer", 89 | "InlineParser", 90 | "InlineState", 91 | "escape", 92 | "escape_url", 93 | "safe_entity", 94 | "unikey", 95 | "html", 96 | "create_markdown", 97 | "markdown", 98 | ] 99 | 100 | __version__ = "3.1.3" 101 | __homepage__ = "https://mistune.lepture.com/" 102 | -------------------------------------------------------------------------------- /src/mistune/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from typing import TYPE_CHECKING, Optional 4 | 5 | from . import __version__ as version 6 | from . import create_markdown 7 | from .renderers.markdown import MarkdownRenderer 8 | from .renderers.rst import RSTRenderer 9 | 10 | if TYPE_CHECKING: 11 | from .core import BaseRenderer 12 | from .markdown import Markdown 13 | 14 | 15 | def _md(args: argparse.Namespace) -> "Markdown": 16 | if args.plugin: 17 | plugins = args.plugin 18 | else: 19 | # default plugins 20 | plugins = ["strikethrough", "footnotes", "table", "speedup"] 21 | 22 | if args.renderer == "rst": 23 | renderer: "BaseRenderer" = RSTRenderer() 24 | elif args.renderer == "markdown": 25 | renderer = MarkdownRenderer() 26 | else: 27 | renderer = args.renderer 28 | return create_markdown( 29 | escape=args.escape, 30 | hard_wrap=args.hardwrap, 31 | renderer=renderer, 32 | plugins=plugins, 33 | ) 34 | 35 | 36 | def _output(text: str, args: argparse.Namespace) -> None: 37 | if args.output: 38 | with open(args.output, "w") as f: 39 | f.write(text) 40 | else: 41 | print(text) 42 | 43 | 44 | CMD_HELP = """Mistune, a sane and fast python markdown parser. 45 | 46 | Here are some use cases of the command line tool: 47 | 48 | $ python -m mistune -m "Hi **Markdown**" 49 |

Hi Markdown

50 | 51 | $ python -m mistune -f README.md 52 |

... 53 | 54 | $ cat README.md | python -m mistune 55 |

... 56 | """ 57 | 58 | 59 | def cli() -> None: 60 | parser = argparse.ArgumentParser( 61 | prog="python -m mistune", 62 | description=CMD_HELP, 63 | formatter_class=argparse.RawDescriptionHelpFormatter, 64 | ) 65 | parser.add_argument( 66 | "-m", 67 | "--message", 68 | help="the markdown message to convert", 69 | ) 70 | parser.add_argument( 71 | "-f", 72 | "--file", 73 | help="the markdown file to convert", 74 | ) 75 | parser.add_argument( 76 | "-p", 77 | "--plugin", 78 | metavar="NAME", 79 | action="extend", 80 | nargs="+", 81 | help="specifiy a plugin to use", 82 | ) 83 | parser.add_argument( 84 | "--escape", 85 | action="store_true", 86 | help="turn on escape option", 87 | ) 88 | parser.add_argument( 89 | "--hardwrap", 90 | action="store_true", 91 | help="turn on hardwrap option", 92 | ) 93 | parser.add_argument( 94 | "-o", 95 | "--output", 96 | help="write the rendered result into file", 97 | ) 98 | parser.add_argument( 99 | "-r", 100 | "--renderer", 101 | default="html", 102 | help="specify the output renderer", 103 | ) 104 | parser.add_argument("--version", action="version", version="mistune " + version) 105 | args = parser.parse_args() 106 | 107 | message = args.message 108 | if not message and not args.file: 109 | message = read_stdin() 110 | 111 | if message: 112 | md = _md(args) 113 | text = md(message) 114 | assert isinstance(text, str) 115 | _output(text, args) 116 | elif args.file: 117 | md = _md(args) 118 | text = md.read(args.file)[0] 119 | assert isinstance(text, str) 120 | _output(text, args) 121 | else: 122 | print("You MUST specify a message or file") 123 | sys.exit(1) 124 | 125 | 126 | def read_stdin() -> Optional[str]: 127 | is_stdin_pipe = not sys.stdin.isatty() 128 | if is_stdin_pipe: 129 | return sys.stdin.read() 130 | else: 131 | return None 132 | 133 | 134 | if __name__ == "__main__": 135 | cli() 136 | -------------------------------------------------------------------------------- /src/mistune/directives/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ._base import BaseDirective, DirectiveParser, DirectivePlugin 4 | from ._fenced import FencedDirective 5 | from ._rst import RSTDirective 6 | from .admonition import Admonition 7 | from .image import Figure, Image 8 | from .include import Include 9 | from .toc import TableOfContents 10 | 11 | 12 | class RstDirective(RSTDirective): # pragma: no cover 13 | def __init__(self, plugins: List[DirectivePlugin]) -> None: 14 | super(RstDirective, self).__init__(plugins) 15 | import warnings 16 | 17 | warnings.warn( 18 | "'RstDirective' is deprecated, please use 'RSTDirective' instead.", 19 | DeprecationWarning, 20 | stacklevel=2, 21 | ) 22 | 23 | 24 | __all__ = [ 25 | "DirectiveParser", 26 | "BaseDirective", 27 | "DirectivePlugin", 28 | "RSTDirective", 29 | "FencedDirective", 30 | "Admonition", 31 | "TableOfContents", 32 | "Include", 33 | "Image", 34 | "Figure", 35 | ] 36 | -------------------------------------------------------------------------------- /src/mistune/directives/_base.py: -------------------------------------------------------------------------------- 1 | import re 2 | from abc import ABCMeta, abstractmethod 3 | from typing import ( 4 | TYPE_CHECKING, 5 | Any, 6 | Callable, 7 | Dict, 8 | Iterable, 9 | List, 10 | Match, 11 | Optional, 12 | Tuple, 13 | Type, 14 | Union, 15 | ) 16 | 17 | if TYPE_CHECKING: 18 | from ..block_parser import BlockParser 19 | from ..core import BlockState 20 | from ..markdown import Markdown 21 | 22 | 23 | class DirectiveParser(ABCMeta): 24 | name = "directive" 25 | 26 | @staticmethod 27 | @abstractmethod 28 | def parse_type(m: Match[str]) -> str: 29 | raise NotImplementedError() 30 | 31 | @staticmethod 32 | @abstractmethod 33 | def parse_title(m: Match[str]) -> str: 34 | raise NotImplementedError() 35 | 36 | @staticmethod 37 | @abstractmethod 38 | def parse_content(m: Match[str]) -> str: 39 | raise NotImplementedError() 40 | 41 | @classmethod 42 | def parse_tokens(cls, block: "BlockParser", text: str, state: "BlockState") -> Iterable[Dict[str, Any]]: 43 | if state.depth() >= block.max_nested_level - 1 and cls.name in block.rules: 44 | rules = list(block.rules) 45 | rules.remove(cls.name) 46 | else: 47 | rules = block.rules 48 | child = state.child_state(text) 49 | block.parse(child, rules) 50 | return child.tokens 51 | 52 | @staticmethod 53 | def parse_options(m: Match[str]) -> List[Tuple[str, str]]: 54 | text = m.group("options") 55 | if not text.strip(): 56 | return [] 57 | 58 | options = [] 59 | for line in re.split(r"\n+", text): 60 | line = line.strip()[1:] 61 | if not line: 62 | continue 63 | i = line.find(":") 64 | k = line[:i] 65 | v = line[i + 1 :].strip() 66 | options.append((k, v)) 67 | return options 68 | 69 | 70 | class BaseDirective(metaclass=ABCMeta): 71 | parser: Type[DirectiveParser] 72 | directive_pattern: Optional[str] = None 73 | 74 | def __init__(self, plugins: List["DirectivePlugin"]): 75 | self._methods: Dict[ 76 | str, 77 | Callable[ 78 | ["BlockParser", Match[str], "BlockState"], 79 | Union[Dict[str, Any], List[Dict[str, Any]]], 80 | ], 81 | ] = {} 82 | self.__plugins = plugins 83 | 84 | def register( 85 | self, 86 | name: str, 87 | fn: Callable[ 88 | ["BlockParser", Match[str], "BlockState"], 89 | Union[Dict[str, Any], List[Dict[str, Any]]], 90 | ], 91 | ) -> None: 92 | self._methods[name] = fn 93 | 94 | def parse_method( 95 | self, block: "BlockParser", m: Match[str], state: "BlockState" 96 | ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: 97 | _type = self.parser.parse_type(m) 98 | method = self._methods.get(_type) 99 | if method: 100 | try: 101 | token = method(block, m, state) 102 | except ValueError as e: 103 | token = {"type": "block_error", "raw": str(e)} 104 | else: 105 | text = m.group(0) 106 | token = { 107 | "type": "block_error", 108 | "raw": text, 109 | } 110 | 111 | if isinstance(token, list): 112 | for tok in token: 113 | state.append_token(tok) 114 | else: 115 | state.append_token(token) 116 | return token 117 | 118 | @abstractmethod 119 | def parse_directive(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: 120 | raise NotImplementedError() 121 | 122 | def register_block_parser(self, md: "Markdown", before: Optional[str] = None) -> None: 123 | md.block.register( 124 | self.parser.name, 125 | self.directive_pattern, 126 | self.parse_directive, 127 | before=before, 128 | ) 129 | 130 | def __call__(self, markdown: "Markdown") -> None: 131 | for plugin in self.__plugins: 132 | plugin.parser = self.parser 133 | plugin(self, markdown) 134 | 135 | 136 | class DirectivePlugin: 137 | parser: Type[DirectiveParser] 138 | 139 | def __init__(self) -> None: ... 140 | 141 | def parse_options(self, m: Match[str]) -> List[Tuple[str, str]]: 142 | return self.parser.parse_options(m) 143 | 144 | def parse_type(self, m: Match[str]) -> str: 145 | return self.parser.parse_type(m) 146 | 147 | def parse_title(self, m: Match[str]) -> str: 148 | return self.parser.parse_title(m) 149 | 150 | def parse_content(self, m: Match[str]) -> str: 151 | return self.parser.parse_content(m) 152 | 153 | def parse_tokens(self, block: "BlockParser", text: str, state: "BlockState") -> Iterable[Dict[str, Any]]: 154 | return self.parser.parse_tokens(block, text, state) 155 | 156 | def parse( 157 | self, block: "BlockParser", m: Match[str], state: "BlockState" 158 | ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: 159 | raise NotImplementedError() 160 | 161 | def __call__(self, directive: BaseDirective, md: "Markdown") -> None: 162 | raise NotImplementedError() 163 | -------------------------------------------------------------------------------- /src/mistune/directives/_fenced.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, List, Match, Optional 3 | 4 | from ._base import BaseDirective, DirectiveParser, DirectivePlugin 5 | 6 | if TYPE_CHECKING: 7 | from ..block_parser import BlockParser 8 | from ..core import BlockState 9 | from ..markdown import Markdown 10 | 11 | 12 | __all__ = ["FencedDirective"] 13 | 14 | 15 | _type_re = re.compile(r"^ *\{[a-zA-Z0-9_-]+\}") 16 | _directive_re = re.compile( 17 | r"\{(?P[a-zA-Z0-9_-]+)\} *(?P[^\n]*)(?:\n|$)" 18 | r"(?P<options>(?:\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)" 19 | r"\n*(?P<text>(?:[^\n]*\n+)*)" 20 | ) 21 | 22 | 23 | class FencedParser(DirectiveParser): 24 | name = "fenced_directive" 25 | 26 | @staticmethod 27 | def parse_type(m: Match[str]) -> str: 28 | return m.group("type") 29 | 30 | @staticmethod 31 | def parse_title(m: Match[str]) -> str: 32 | return m.group("title") 33 | 34 | @staticmethod 35 | def parse_content(m: Match[str]) -> str: 36 | return m.group("text") 37 | 38 | 39 | class FencedDirective(BaseDirective): 40 | """A **fenced** style of directive looks like a fenced code block, it is 41 | inspired by markdown-it-docutils. The syntax looks like: 42 | 43 | .. code-block:: text 44 | 45 | ```{directive-type} title 46 | :option-key: option value 47 | :option-key: option value 48 | 49 | content text here 50 | ``` 51 | 52 | To use ``FencedDirective``, developers can add it into plugin list in 53 | the :class:`Markdown` instance: 54 | 55 | .. code-block:: python 56 | 57 | import mistune 58 | from mistune.directives import FencedDirective, Admonition 59 | 60 | md = mistune.create_markdown(plugins=[ 61 | # ... 62 | FencedDirective([Admonition()]), 63 | ]) 64 | 65 | FencedDirective is using >= 3 backticks or curly-brackets for the fenced 66 | syntax. Developers can change it to other characters, e.g. colon: 67 | 68 | .. code-block:: python 69 | 70 | directive = FencedDirective([Admonition()], ':') 71 | 72 | And then the directive syntax would look like: 73 | 74 | .. code-block:: text 75 | 76 | ::::{note} Nesting directives 77 | You can nest directives by ensuring the start and end fence matching 78 | the length. For instance, in this example, the admonition is started 79 | with 4 colons, then it should end with 4 colons. 80 | 81 | You can nest another admonition with other length of colons except 4. 82 | 83 | :::{tip} Longer outermost fence 84 | It would be better that you put longer markers for the outer fence, 85 | and shorter markers for the inner fence. In this example, we put 4 86 | colons outsie, and 3 colons inside. 87 | ::: 88 | :::: 89 | 90 | :param plugins: list of directive plugins 91 | :param markers: characters to determine the fence, default is backtick 92 | and curly-bracket 93 | """ 94 | 95 | parser = FencedParser 96 | 97 | def __init__(self, plugins: List[DirectivePlugin], markers: str = "`~") -> None: 98 | super(FencedDirective, self).__init__(plugins) 99 | self.markers = markers 100 | _marker_pattern = "|".join(re.escape(c) for c in markers) 101 | self.directive_pattern = ( 102 | r"^(?P<fenced_directive_mark>(?:" + _marker_pattern + r"){3,})" 103 | r"\{[a-zA-Z0-9_-]+\}" 104 | ) 105 | 106 | def _process_directive(self, block: "BlockParser", marker: str, start: int, state: "BlockState") -> Optional[int]: 107 | mlen = len(marker) 108 | cursor_start = start + len(marker) 109 | 110 | _end_pattern = ( 111 | r"^ {0,3}" + marker[0] + "{" + str(mlen) + r",}" 112 | r"[ \t]*(?:\n|$)" 113 | ) 114 | _end_re = re.compile(_end_pattern, re.M) 115 | 116 | _end_m = _end_re.search(state.src, cursor_start) 117 | if _end_m: 118 | text = state.src[cursor_start : _end_m.start()] 119 | end_pos = _end_m.end() 120 | else: 121 | text = state.src[cursor_start:] 122 | end_pos = state.cursor_max 123 | 124 | m = _directive_re.match(text) 125 | if not m: 126 | return None 127 | 128 | self.parse_method(block, m, state) 129 | return end_pos 130 | 131 | def parse_directive(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: 132 | marker = m.group("fenced_directive_mark") 133 | return self._process_directive(block, marker, m.start(), state) 134 | 135 | def parse_fenced_code(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: 136 | info = m.group("fenced_3") 137 | if not info or not _type_re.match(info): 138 | return block.parse_fenced_code(m, state) 139 | 140 | if state.depth() >= block.max_nested_level: 141 | return block.parse_fenced_code(m, state) 142 | 143 | marker = m.group("fenced_2") 144 | return self._process_directive(block, marker, m.start(), state) 145 | 146 | def __call__(self, md: "Markdown") -> None: 147 | super(FencedDirective, self).__call__(md) 148 | if self.markers == "`~": 149 | md.block.register("fenced_code", None, self.parse_fenced_code) 150 | else: 151 | self.register_block_parser(md, "fenced_code") 152 | -------------------------------------------------------------------------------- /src/mistune/directives/_rst.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, Match, Optional 3 | 4 | from ._base import BaseDirective, DirectiveParser 5 | 6 | if TYPE_CHECKING: 7 | from ..block_parser import BlockParser 8 | from ..core import BlockState 9 | from ..markdown import Markdown 10 | 11 | __all__ = ["RSTDirective"] 12 | 13 | 14 | _directive_re = re.compile( 15 | r"\.\.( +)(?P<type>[a-zA-Z0-9_-]+)\:\: *(?P<title>[^\n]*)(?:\n|$)" 16 | r"(?P<options>(?: \1 {0,3}\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)" 17 | r"\n*(?P<text>(?: \1 {0,3}[^\n]*\n+)*)" 18 | ) 19 | 20 | 21 | class RSTParser(DirectiveParser): 22 | name = "rst_directive" 23 | 24 | @staticmethod 25 | def parse_type(m: Match[str]) -> str: 26 | return m.group("type") 27 | 28 | @staticmethod 29 | def parse_title(m: Match[str]) -> str: 30 | return m.group("title") 31 | 32 | @staticmethod 33 | def parse_content(m: Match[str]) -> str: 34 | full_content = m.group(0) 35 | text = m.group("text") 36 | pretext = full_content[: -len(text)] 37 | leading = len(m.group(1)) + 2 38 | return "\n".join(line[leading:] for line in text.splitlines()) + "\n" 39 | 40 | 41 | class RSTDirective(BaseDirective): 42 | """A RST style of directive syntax is inspired by reStructuredText. 43 | The syntax is very powerful that you can define a lot of custom 44 | features on your own. The syntax looks like: 45 | 46 | .. code-block:: text 47 | 48 | .. directive-type:: directive value 49 | :option-key: option value 50 | :option-key: option value 51 | 52 | content text here 53 | 54 | To use ``RSTDirective``, developers can add it into plugin list in 55 | the :class:`Markdown` instance: 56 | 57 | .. code-block:: python 58 | 59 | import mistune 60 | from mistune.directives import RSTDirective, Admonition 61 | 62 | md = mistune.create_markdown(plugins=[ 63 | # ... 64 | RSTDirective([Admonition()]), 65 | ]) 66 | """ 67 | 68 | parser = RSTParser 69 | directive_pattern = r"^\.\. +[a-zA-Z0-9_-]+\:\:" 70 | 71 | def parse_directive(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: 72 | m2 = _directive_re.match(state.src, state.cursor) 73 | if not m2: 74 | return None 75 | 76 | self.parse_method(block, m2, state) 77 | return m2.end() 78 | 79 | def __call__(self, markdown: "Markdown") -> None: 80 | super(RSTDirective, self).__call__(markdown) 81 | self.register_block_parser(markdown) 82 | -------------------------------------------------------------------------------- /src/mistune/directives/admonition.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Dict, Match 2 | 3 | from ._base import BaseDirective, DirectivePlugin 4 | 5 | if TYPE_CHECKING: 6 | from ..block_parser import BlockParser 7 | from ..core import BlockState 8 | from ..markdown import Markdown 9 | 10 | 11 | class Admonition(DirectivePlugin): 12 | SUPPORTED_NAMES = { 13 | "attention", 14 | "caution", 15 | "danger", 16 | "error", 17 | "hint", 18 | "important", 19 | "note", 20 | "tip", 21 | "warning", 22 | } 23 | 24 | def parse(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Dict[str, Any]: 25 | name = self.parse_type(m) 26 | attrs = {"name": name} 27 | options = dict(self.parse_options(m)) 28 | if "class" in options: 29 | attrs["class"] = options["class"] 30 | 31 | title = self.parse_title(m) 32 | if not title: 33 | title = name.capitalize() 34 | 35 | content = self.parse_content(m) 36 | children = [ 37 | { 38 | "type": "admonition_title", 39 | "text": title, 40 | }, 41 | { 42 | "type": "admonition_content", 43 | "children": self.parse_tokens(block, content, state), 44 | }, 45 | ] 46 | return { 47 | "type": "admonition", 48 | "children": children, 49 | "attrs": attrs, 50 | } 51 | 52 | def __call__(self, directive: "BaseDirective", md: "Markdown") -> None: 53 | for name in self.SUPPORTED_NAMES: 54 | directive.register(name, self.parse) 55 | 56 | assert md.renderer is not None 57 | if md.renderer.NAME == "html": 58 | md.renderer.register("admonition", render_admonition) 59 | md.renderer.register("admonition_title", render_admonition_title) 60 | md.renderer.register("admonition_content", render_admonition_content) 61 | 62 | 63 | def render_admonition(self: Any, text: str, name: str, **attrs: Any) -> str: 64 | html = '<section class="admonition ' + name 65 | _cls = attrs.get("class") 66 | if _cls: 67 | html += " " + _cls 68 | return html + '">\n' + text + "</section>\n" 69 | 70 | 71 | def render_admonition_title(self: Any, text: str) -> str: 72 | return '<p class="admonition-title">' + text + "</p>\n" 73 | 74 | 75 | def render_admonition_content(self: Any, text: str) -> str: 76 | return text 77 | -------------------------------------------------------------------------------- /src/mistune/directives/include.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import TYPE_CHECKING, Any, Dict, List, Match, Union 3 | 4 | from ._base import BaseDirective, DirectivePlugin 5 | 6 | if TYPE_CHECKING: 7 | from ..block_parser import BlockParser 8 | from ..core import BaseRenderer, BlockState 9 | from ..markdown import Markdown 10 | 11 | 12 | class Include(DirectivePlugin): 13 | def parse( 14 | self, block: "BlockParser", m: Match[str], state: "BlockState" 15 | ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: 16 | source_file = state.env.get("__file__") 17 | if not source_file: 18 | return {"type": "block_error", "raw": "Missing source file"} 19 | 20 | encoding = "utf-8" 21 | options = self.parse_options(m) 22 | if options: 23 | attrs = dict(options) 24 | if "encoding" in attrs: 25 | encoding = attrs["encoding"] 26 | else: 27 | attrs = {} 28 | 29 | relpath = self.parse_title(m) 30 | dest = os.path.join(os.path.dirname(source_file), relpath) 31 | dest = os.path.normpath(dest) 32 | 33 | if dest == source_file: 34 | return { 35 | "type": "block_error", 36 | "raw": "Could not include self: " + relpath, 37 | } 38 | 39 | if not os.path.isfile(dest): 40 | return { 41 | "type": "block_error", 42 | "raw": "Could not find file: " + relpath, 43 | } 44 | 45 | with open(dest, "rb") as f: 46 | content = f.read().decode(encoding) 47 | 48 | ext = os.path.splitext(relpath)[1] 49 | if ext in {".md", ".markdown", ".mkd"}: 50 | new_state = block.state_cls() 51 | new_state.env["__file__"] = dest 52 | new_state.process(content) 53 | block.parse(new_state) 54 | return new_state.tokens 55 | 56 | elif ext in {".html", ".xhtml", ".htm"}: 57 | return {"type": "block_html", "raw": content} 58 | 59 | attrs["filepath"] = dest 60 | return { 61 | "type": "include", 62 | "raw": content, 63 | "attrs": attrs, 64 | } 65 | 66 | def __call__(self, directive: BaseDirective, md: "Markdown") -> None: 67 | directive.register("include", self.parse) 68 | if md.renderer and md.renderer.NAME == "html": 69 | md.renderer.register("include", render_html_include) 70 | 71 | 72 | def render_html_include(renderer: "BaseRenderer", text: str, **attrs: Any) -> str: 73 | return '<pre class="directive-include">\n' + text + "</pre>\n" 74 | -------------------------------------------------------------------------------- /src/mistune/directives/toc.py: -------------------------------------------------------------------------------- 1 | """ 2 | TOC directive 3 | ~~~~~~~~~~~~~ 4 | 5 | The TOC directive syntax looks like:: 6 | 7 | .. toc:: Title 8 | :min-level: 1 9 | :max-level: 3 10 | 11 | "Title", "min-level", and "max-level" option can be empty. "min-level" 12 | and "max-level" are integers >= 1 and <= 6, which define the allowed 13 | heading levels writers want to include in the table of contents. 14 | """ 15 | 16 | from typing import TYPE_CHECKING, Any, Dict, Match 17 | 18 | from ..toc import normalize_toc_item, render_toc_ul 19 | from ._base import BaseDirective, DirectivePlugin 20 | 21 | if TYPE_CHECKING: 22 | from ..block_parser import BlockParser 23 | from ..core import BaseRenderer, BlockState 24 | from ..markdown import Markdown 25 | 26 | 27 | class TableOfContents(DirectivePlugin): 28 | def __init__(self, min_level: int = 1, max_level: int = 3) -> None: 29 | self.min_level = min_level 30 | self.max_level = max_level 31 | 32 | def generate_heading_id(self, token: Dict[str, Any], index: int) -> str: 33 | return "toc_" + str(index + 1) 34 | 35 | def parse(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Dict[str, Any]: 36 | title = self.parse_title(m) 37 | options = self.parse_options(m) 38 | if options: 39 | d_options = dict(options) 40 | collapse = "collapse" in d_options 41 | min_level = _normalize_level(d_options, "min-level", self.min_level) 42 | max_level = _normalize_level(d_options, "max-level", self.max_level) 43 | if min_level < self.min_level: 44 | raise ValueError(f'"min-level" option MUST be >= {self.min_level}') 45 | if max_level > self.max_level: 46 | raise ValueError(f'"max-level" option MUST be <= {self.max_level}') 47 | if min_level > max_level: 48 | raise ValueError('"min-level" option MUST be less than "max-level" option') 49 | else: 50 | collapse = False 51 | min_level = self.min_level 52 | max_level = self.max_level 53 | 54 | attrs = { 55 | "min_level": min_level, 56 | "max_level": max_level, 57 | "collapse": collapse, 58 | } 59 | return {"type": "toc", "text": title or "", "attrs": attrs} 60 | 61 | def toc_hook(self, md: "Markdown", state: "BlockState") -> None: 62 | sections = [] 63 | headings = [] 64 | 65 | for tok in state.tokens: 66 | if tok["type"] == "toc": 67 | sections.append(tok) 68 | elif tok["type"] == "heading": 69 | headings.append(tok) 70 | 71 | if sections: 72 | toc_items = [] 73 | # adding ID for each heading 74 | for i, tok in enumerate(headings): 75 | tok["attrs"]["id"] = self.generate_heading_id(tok, i) 76 | toc_items.append(normalize_toc_item(md, tok)) 77 | 78 | for sec in sections: 79 | _min = sec["attrs"]["min_level"] 80 | _max = sec["attrs"]["max_level"] 81 | toc = [item for item in toc_items if _min <= item[0] <= _max] 82 | sec["attrs"]["toc"] = toc 83 | 84 | def __call__(self, directive: BaseDirective, md: "Markdown") -> None: 85 | if md.renderer and md.renderer.NAME == "html": 86 | # only works with HTML renderer 87 | directive.register("toc", self.parse) 88 | md.before_render_hooks.append(self.toc_hook) 89 | md.renderer.register("toc", render_html_toc) 90 | 91 | 92 | def render_html_toc(renderer: "BaseRenderer", title: str, collapse: bool = False, **attrs: Any) -> str: 93 | if not title: 94 | title = "Table of Contents" 95 | content = render_toc_ul(attrs["toc"]) 96 | 97 | html = '<details class="toc"' 98 | if not collapse: 99 | html += " open" 100 | html += ">\n<summary>" + title + "</summary>\n" 101 | return html + content + "</details>\n" 102 | 103 | 104 | def _normalize_level(options: Dict[str, Any], name: str, default: Any) -> Any: 105 | level = options.get(name) 106 | if not level: 107 | return default 108 | try: 109 | return int(level) 110 | except (ValueError, TypeError): 111 | raise ValueError(f'"{name}" option MUST be integer') 112 | -------------------------------------------------------------------------------- /src/mistune/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import string 3 | from typing import Any, Dict, Tuple, Union 4 | 5 | from .util import escape_url 6 | 7 | PREVENT_BACKSLASH = r"(?<!\\)(?:\\\\)*" 8 | PUNCTUATION = r"[" + re.escape(string.punctuation) + r"]" 9 | 10 | LINK_LABEL = r"(?:[^\\\[\]]|\\.){0,500}" 11 | 12 | LINK_BRACKET_START = re.compile(r"[ \t]*\n?[ \t]*<") 13 | LINK_BRACKET_RE = re.compile(r"<([^<>\n\\\x00]*)>") 14 | LINK_HREF_BLOCK_RE = re.compile(r"[ \t]*\n?[ \t]*([^\s]+)(?:\s|$)") 15 | LINK_HREF_INLINE_RE = re.compile( 16 | r"[ \t]*\n?[ \t]*([^ \t\n]*?)(?:[ \t\n]|" 17 | r"(?:" + PREVENT_BACKSLASH + r"\)))" 18 | ) 19 | 20 | LINK_TITLE_RE = re.compile( 21 | r"[ \t\n]+(" 22 | r'"(?:\\' + PUNCTUATION + r'|[^"\x00])*"|' # "title" 23 | r"'(?:\\" + PUNCTUATION + r"|[^'\x00])*'" # 'title' 24 | r")" 25 | ) 26 | PAREN_END_RE = re.compile(r"\s*\)") 27 | 28 | HTML_TAGNAME = r"[A-Za-z][A-Za-z0-9-]*" 29 | HTML_ATTRIBUTES = ( 30 | r"(?:\s+[A-Za-z_:][A-Za-z0-9_.:-]*" 31 | r'(?:\s*=\s*(?:[^ !"\'=<>`]+|\'[^\']*?\'|"[^\"]*?"))?)*' 32 | ) 33 | 34 | BLOCK_TAGS = ( 35 | "address", 36 | "article", 37 | "aside", 38 | "base", 39 | "basefont", 40 | "blockquote", 41 | "body", 42 | "caption", 43 | "center", 44 | "col", 45 | "colgroup", 46 | "dd", 47 | "details", 48 | "dialog", 49 | "dir", 50 | "div", 51 | "dl", 52 | "dt", 53 | "fieldset", 54 | "figcaption", 55 | "figure", 56 | "footer", 57 | "form", 58 | "frame", 59 | "frameset", 60 | "h1", 61 | "h2", 62 | "h3", 63 | "h4", 64 | "h5", 65 | "h6", 66 | "head", 67 | "header", 68 | "hr", 69 | "html", 70 | "iframe", 71 | "legend", 72 | "li", 73 | "link", 74 | "main", 75 | "menu", 76 | "menuitem", 77 | "meta", 78 | "nav", 79 | "noframes", 80 | "ol", 81 | "optgroup", 82 | "option", 83 | "p", 84 | "param", 85 | "section", 86 | "source", 87 | "summary", 88 | "table", 89 | "tbody", 90 | "td", 91 | "tfoot", 92 | "th", 93 | "thead", 94 | "title", 95 | "tr", 96 | "track", 97 | "ul", 98 | ) 99 | PRE_TAGS = ("pre", "script", "style", "textarea") 100 | 101 | _INLINE_LINK_LABEL_RE = re.compile(LINK_LABEL + r"\]") 102 | _INLINE_SQUARE_BRACKET_RE = re.compile(PREVENT_BACKSLASH + r"[\[\]]") 103 | _ESCAPE_CHAR_RE = re.compile(r"\\(" + PUNCTUATION + r")") 104 | 105 | 106 | def unescape_char(text: str) -> str: 107 | return _ESCAPE_CHAR_RE.sub(r"\1", text) 108 | 109 | 110 | def parse_link_text(src: str, pos: int) -> Union[Tuple[str, int], Tuple[None, None]]: 111 | level = 1 112 | found = False 113 | start_pos = pos 114 | 115 | while pos < len(src): 116 | m = _INLINE_SQUARE_BRACKET_RE.search(src, pos) 117 | if not m: 118 | break 119 | 120 | pos = m.end() 121 | marker = m.group(0) 122 | if marker == "]": 123 | level -= 1 124 | if level == 0: 125 | found = True 126 | break 127 | else: 128 | level += 1 129 | 130 | if found: 131 | text = src[start_pos : pos - 1] 132 | return text, pos 133 | return None, None 134 | 135 | 136 | def parse_link_label(src: str, start_pos: int) -> Union[Tuple[str, int], Tuple[None, None]]: 137 | m = _INLINE_LINK_LABEL_RE.match(src, start_pos) 138 | if m: 139 | label = m.group(0)[:-1] 140 | return label, m.end() 141 | return None, None 142 | 143 | 144 | def parse_link_href(src: str, start_pos: int, block: bool = False) -> Union[Tuple[str, int], Tuple[None, None]]: 145 | m = LINK_BRACKET_START.match(src, start_pos) 146 | if m: 147 | start_pos = m.end() - 1 148 | m = LINK_BRACKET_RE.match(src, start_pos) 149 | if m: 150 | return m.group(1), m.end() 151 | return None, None 152 | 153 | if block: 154 | m = LINK_HREF_BLOCK_RE.match(src, start_pos) 155 | else: 156 | m = LINK_HREF_INLINE_RE.match(src, start_pos) 157 | 158 | if not m: 159 | return None, None 160 | 161 | end_pos = m.end() 162 | href = m.group(1) 163 | 164 | if block and src[end_pos - 1] == href[-1]: 165 | return href, end_pos 166 | return href, end_pos - 1 167 | 168 | 169 | def parse_link_title(src: str, start_pos: int, max_pos: int) -> Union[Tuple[str, int], Tuple[None, None]]: 170 | m = LINK_TITLE_RE.match(src, start_pos, max_pos) 171 | if m: 172 | title = m.group(1)[1:-1] 173 | title = unescape_char(title) 174 | return title, m.end() 175 | return None, None 176 | 177 | 178 | def parse_link(src: str, pos: int) -> Union[Tuple[Dict[str, Any], int], Tuple[None, None]]: 179 | href, href_pos = parse_link_href(src, pos) 180 | if href is None: 181 | return None, None 182 | assert href_pos is not None 183 | title, title_pos = parse_link_title(src, href_pos, len(src)) 184 | next_pos = title_pos or href_pos 185 | m = PAREN_END_RE.match(src, next_pos) 186 | if not m: 187 | return None, None 188 | 189 | href = unescape_char(href) 190 | attrs = {"url": escape_url(href)} 191 | if title: 192 | attrs["title"] = title 193 | return attrs, m.end() 194 | -------------------------------------------------------------------------------- /src/mistune/markdown.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union 2 | 3 | from .block_parser import BlockParser 4 | from .core import BaseRenderer, BlockState 5 | from .inline_parser import InlineParser 6 | from .plugins import Plugin 7 | 8 | 9 | class Markdown: 10 | """Markdown instance to convert markdown text into HTML or other formats. 11 | Here is an example with the HTMLRenderer:: 12 | 13 | from mistune import HTMLRenderer 14 | 15 | md = Markdown(renderer=HTMLRenderer(escape=False)) 16 | md('hello **world**') 17 | 18 | :param renderer: a renderer to convert parsed tokens 19 | :param block: block level syntax parser 20 | :param inline: inline level syntax parser 21 | :param plugins: mistune plugins to use 22 | """ 23 | 24 | def __init__( 25 | self, 26 | renderer: Optional[BaseRenderer] = None, 27 | block: Optional[BlockParser] = None, 28 | inline: Optional[InlineParser] = None, 29 | plugins: Optional[Iterable[Plugin]] = None, 30 | ): 31 | if block is None: 32 | block = BlockParser() 33 | 34 | if inline is None: 35 | inline = InlineParser() 36 | 37 | self.renderer = renderer 38 | self.block: BlockParser = block 39 | self.inline: InlineParser = inline 40 | self.before_parse_hooks: List[Callable[["Markdown", BlockState], None]] = [] 41 | self.before_render_hooks: List[Callable[["Markdown", BlockState], Any]] = [] 42 | self.after_render_hooks: List[ 43 | Callable[["Markdown", Union[str, List[Dict[str, Any]]], BlockState], Union[str, List[Dict[str, Any]]]] 44 | ] = [] 45 | 46 | if plugins: 47 | for plugin in plugins: 48 | plugin(self) 49 | 50 | def use(self, plugin: Plugin) -> None: 51 | plugin(self) 52 | 53 | def render_state(self, state: BlockState) -> Union[str, List[Dict[str, Any]]]: 54 | data = self._iter_render(state.tokens, state) 55 | if self.renderer: 56 | return self.renderer(data, state) 57 | return list(data) 58 | 59 | def _iter_render(self, tokens: Iterable[Dict[str, Any]], state: BlockState) -> Iterable[Dict[str, Any]]: 60 | for tok in tokens: 61 | if "children" in tok: 62 | children = self._iter_render(tok["children"], state) 63 | tok["children"] = list(children) 64 | elif "text" in tok: 65 | text = tok.pop("text") 66 | # process inline text 67 | # avoid striping emsp or other unicode spaces 68 | tok["children"] = self.inline(text.strip(" \r\n\t\f"), state.env) 69 | yield tok 70 | 71 | def parse(self, s: str, state: Optional[BlockState] = None) -> Tuple[Union[str, List[Dict[str, Any]]], BlockState]: 72 | """Parse and convert the given markdown string. If renderer is None, 73 | the returned **result** will be parsed markdown tokens. 74 | 75 | :param s: markdown string 76 | :param state: instance of BlockState 77 | :returns: result, state 78 | """ 79 | if state is None: 80 | state = self.block.state_cls() 81 | 82 | # normalize line separator 83 | s = s.replace("\r\n", "\n") 84 | s = s.replace("\r", "\n") 85 | if not s.endswith("\n"): 86 | s += "\n" 87 | 88 | state.process(s) 89 | 90 | for hook in self.before_parse_hooks: 91 | hook(self, state) 92 | 93 | self.block.parse(state) 94 | 95 | for hook2 in self.before_render_hooks: 96 | hook2(self, state) 97 | 98 | result = self.render_state(state) 99 | 100 | for hook3 in self.after_render_hooks: 101 | result = hook3(self, result, state) 102 | return result, state 103 | 104 | def read( 105 | self, filepath: str, encoding: str = "utf-8", state: Optional[BlockState] = None 106 | ) -> Tuple[Union[str, List[Dict[str, Any]]], BlockState]: 107 | if state is None: 108 | state = self.block.state_cls() 109 | 110 | state.env["__file__"] = filepath 111 | with open(filepath, "rb") as f: 112 | s = f.read() 113 | 114 | s2 = s.decode(encoding) 115 | return self.parse(s2, state) 116 | 117 | def __call__(self, s: str) -> Union[str, List[Dict[str, Any]]]: 118 | if s is None: 119 | s = "\n" 120 | return self.parse(s)[0] 121 | -------------------------------------------------------------------------------- /src/mistune/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from typing import TYPE_CHECKING, Dict, Protocol, Union, cast 3 | 4 | if TYPE_CHECKING: 5 | from ..markdown import Markdown 6 | 7 | _plugins = { 8 | "speedup": "mistune.plugins.speedup.speedup", 9 | "strikethrough": "mistune.plugins.formatting.strikethrough", 10 | "mark": "mistune.plugins.formatting.mark", 11 | "insert": "mistune.plugins.formatting.insert", 12 | "superscript": "mistune.plugins.formatting.superscript", 13 | "subscript": "mistune.plugins.formatting.subscript", 14 | "footnotes": "mistune.plugins.footnotes.footnotes", 15 | "table": "mistune.plugins.table.table", 16 | "url": "mistune.plugins.url.url", 17 | "abbr": "mistune.plugins.abbr.abbr", 18 | "def_list": "mistune.plugins.def_list.def_list", 19 | "math": "mistune.plugins.math.math", 20 | "ruby": "mistune.plugins.ruby.ruby", 21 | "task_lists": "mistune.plugins.task_lists.task_lists", 22 | "spoiler": "mistune.plugins.spoiler.spoiler", 23 | } 24 | 25 | 26 | class Plugin(Protocol): 27 | def __call__(self, md: "Markdown") -> None: ... 28 | 29 | 30 | _cached_modules: Dict[str, Plugin] = {} 31 | 32 | PluginRef = Union[str, Plugin] # reference to register a plugin 33 | 34 | 35 | def import_plugin(name: PluginRef) -> Plugin: 36 | if callable(name): 37 | return name 38 | 39 | if name in _cached_modules: 40 | return _cached_modules[name] 41 | 42 | if name in _plugins: 43 | module_path, func_name = _plugins[name].rsplit(".", 1) 44 | else: 45 | module_path, func_name = name.rsplit(".", 1) 46 | 47 | module = import_module(module_path) 48 | plugin = cast(Plugin, getattr(module, func_name)) 49 | _cached_modules[name] = plugin 50 | return plugin 51 | -------------------------------------------------------------------------------- /src/mistune/plugins/abbr.py: -------------------------------------------------------------------------------- 1 | import re 2 | import types 3 | from typing import TYPE_CHECKING, Match 4 | 5 | from ..helpers import PREVENT_BACKSLASH 6 | from ..util import escape 7 | 8 | if TYPE_CHECKING: 9 | from ..block_parser import BlockParser 10 | from ..core import BaseRenderer, BlockState, InlineState 11 | from ..inline_parser import InlineParser 12 | from ..markdown import Markdown 13 | 14 | __all__ = ["abbr"] 15 | 16 | # https://michelf.ca/projects/php-markdown/extra/#abbr 17 | REF_ABBR = ( 18 | r"^ {0,3}\*\[(?P<abbr_key>[^\]]+)" + PREVENT_BACKSLASH + r"\]:" 19 | r"(?P<abbr_text>(?:[ \t]*\n(?: {3,}|\t)[^\n]+)|(?:[^\n]*))$" 20 | ) 21 | 22 | 23 | def parse_ref_abbr(block: "BlockParser", m: Match[str], state: "BlockState") -> int: 24 | ref = state.env.get("ref_abbrs") 25 | if not ref: 26 | ref = {} 27 | key = m.group("abbr_key") 28 | text = m.group("abbr_text") 29 | ref[key] = text.strip() 30 | state.env["ref_abbrs"] = ref 31 | # abbr definition can split paragraph 32 | state.append_token({"type": "blank_line"}) 33 | return m.end() + 1 34 | 35 | 36 | def process_text(inline: "InlineParser", text: str, state: "InlineState") -> None: 37 | ref = state.env.get("ref_abbrs") 38 | if not ref: 39 | return state.append_token({"type": "text", "raw": text}) 40 | 41 | if state.tokens: 42 | last = state.tokens[-1] 43 | if last["type"] == "text": 44 | state.tokens.pop() 45 | text = last["raw"] + text 46 | 47 | abbrs_re = state.env.get("abbrs_re") 48 | if not abbrs_re: 49 | abbrs_re = re.compile(r"|".join(re.escape(k) for k in ref.keys())) 50 | state.env["abbrs_re"] = abbrs_re 51 | 52 | pos = 0 53 | while pos < len(text): 54 | m = abbrs_re.search(text, pos) 55 | if not m: 56 | break 57 | 58 | end_pos = m.start() 59 | if end_pos > pos: 60 | hole = text[pos:end_pos] 61 | state.append_token({"type": "text", "raw": hole}) 62 | 63 | label = m.group(0) 64 | state.append_token( 65 | {"type": "abbr", "children": [{"type": "text", "raw": label}], "attrs": {"title": ref[label]}} 66 | ) 67 | pos = m.end() 68 | 69 | if pos == 0: 70 | # special case, just pure text 71 | state.append_token({"type": "text", "raw": text}) 72 | elif pos < len(text): 73 | state.append_token({"type": "text", "raw": text[pos:]}) 74 | 75 | 76 | def render_abbr(renderer: "BaseRenderer", text: str, title: str) -> str: 77 | if not title: 78 | return "<abbr>" + text + "</abbr>" 79 | return '<abbr title="' + escape(title) + '">' + text + "</abbr>" 80 | 81 | 82 | def abbr(md: "Markdown") -> None: 83 | """A mistune plugin to support abbreviations, spec defined at 84 | https://michelf.ca/projects/php-markdown/extra/#abbr 85 | 86 | Here is an example: 87 | 88 | .. code-block:: text 89 | 90 | The HTML specification 91 | is maintained by the W3C. 92 | 93 | *[HTML]: Hyper Text Markup Language 94 | *[W3C]: World Wide Web Consortium 95 | 96 | It will be converted into HTML: 97 | 98 | .. code-block:: html 99 | 100 | The <abbr title="Hyper Text Markup Language">HTML</abbr> specification 101 | is maintained by the <abbr title="World Wide Web Consortium">W3C</abbr>. 102 | 103 | :param md: Markdown instance 104 | """ 105 | md.block.register("ref_abbr", REF_ABBR, parse_ref_abbr, before="paragraph") 106 | # replace process_text 107 | md.inline.process_text = types.MethodType(process_text, md.inline) # type: ignore[method-assign] 108 | if md.renderer and md.renderer.NAME == "html": 109 | md.renderer.register("abbr", render_abbr) 110 | -------------------------------------------------------------------------------- /src/mistune/plugins/def_list.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Match 3 | 4 | from ..util import strip_end 5 | 6 | if TYPE_CHECKING: 7 | from ..block_parser import BlockParser 8 | from ..core import BaseRenderer, BlockState 9 | from ..markdown import Markdown 10 | 11 | __all__ = ["def_list"] 12 | 13 | # https://michelf.ca/projects/php-markdown/extra/#def-list 14 | 15 | DEF_PATTERN = ( 16 | r"^(?P<def_list_head>(?:[^\n]+\n)+?)" 17 | r"\n?(?:" 18 | r"\:[ \t]+.*\n" 19 | r"(?:[^\n]+\n)*" # lazy continue line 20 | r"(?:(?:[ \t]*\n)*[ \t]+[^\n]+\n)*" 21 | r"(?:[ \t]*\n)*" 22 | r")+" 23 | ) 24 | DEF_RE = re.compile(DEF_PATTERN, re.M) 25 | DD_START_RE = re.compile(r"^:[ \t]+", re.M) 26 | TRIM_RE = re.compile(r"^ {0,4}", re.M) 27 | HAS_BLANK_LINE_RE = re.compile(r"\n[ \t]*\n$") 28 | 29 | 30 | def parse_def_list(block: "BlockParser", m: Match[str], state: "BlockState") -> int: 31 | pos = m.end() 32 | children = list(_parse_def_item(block, m)) 33 | 34 | m2 = DEF_RE.match(state.src, pos) 35 | while m2: 36 | children.extend(list(_parse_def_item(block, m2))) 37 | pos = m2.end() 38 | m2 = DEF_RE.match(state.src, pos) 39 | 40 | state.append_token( 41 | { 42 | "type": "def_list", 43 | "children": children, 44 | } 45 | ) 46 | return pos 47 | 48 | 49 | def _parse_def_item(block: "BlockParser", m: Match[str]) -> Iterable[Dict[str, Any]]: 50 | head = m.group("def_list_head") 51 | for line in head.splitlines(): 52 | yield { 53 | "type": "def_list_head", 54 | "text": line, 55 | } 56 | 57 | src = m.group(0) 58 | end = len(head) 59 | 60 | m2 = DD_START_RE.search(src, end) 61 | assert m2 is not None 62 | start = m2.start() 63 | prev_blank_line = src[end:start] == "\n" 64 | while m2: 65 | m2 = DD_START_RE.search(src, start + 1) 66 | if not m2: 67 | break 68 | 69 | end = m2.start() 70 | text = src[start:end].replace(":", " ", 1) 71 | children = _process_text(block, text, prev_blank_line) 72 | prev_blank_line = bool(HAS_BLANK_LINE_RE.search(text)) 73 | yield { 74 | "type": "def_list_item", 75 | "children": children, 76 | } 77 | start = end 78 | 79 | text = src[start:].replace(":", " ", 1) 80 | children = _process_text(block, text, prev_blank_line) 81 | yield { 82 | "type": "def_list_item", 83 | "children": children, 84 | } 85 | 86 | 87 | def _process_text(block: "BlockParser", text: str, loose: bool) -> List[Any]: 88 | text = TRIM_RE.sub("", text) 89 | state = block.state_cls() 90 | state.process(strip_end(text)) 91 | # use default list rules 92 | block.parse(state, block.list_rules) 93 | tokens = state.tokens 94 | if not loose and len(tokens) == 1 and tokens[0]["type"] == "paragraph": 95 | tokens[0]["type"] = "block_text" 96 | return tokens 97 | 98 | 99 | def render_def_list(renderer: "BaseRenderer", text: str) -> str: 100 | return "<dl>\n" + text + "</dl>\n" 101 | 102 | 103 | def render_def_list_head(renderer: "BaseRenderer", text: str) -> str: 104 | return "<dt>" + text + "</dt>\n" 105 | 106 | 107 | def render_def_list_item(renderer: "BaseRenderer", text: str) -> str: 108 | return "<dd>" + text + "</dd>\n" 109 | 110 | 111 | def def_list(md: "Markdown") -> None: 112 | """A mistune plugin to support def list, spec defined at 113 | https://michelf.ca/projects/php-markdown/extra/#def-list 114 | 115 | Here is an example: 116 | 117 | .. code-block:: text 118 | 119 | Apple 120 | : Pomaceous fruit of plants of the genus Malus in 121 | the family Rosaceae. 122 | 123 | Orange 124 | : The fruit of an evergreen tree of the genus Citrus. 125 | 126 | It will be converted into HTML: 127 | 128 | .. code-block:: html 129 | 130 | <dl> 131 | <dt>Apple</dt> 132 | <dd>Pomaceous fruit of plants of the genus Malus in 133 | the family Rosaceae.</dd> 134 | 135 | <dt>Orange</dt> 136 | <dd>The fruit of an evergreen tree of the genus Citrus.</dd> 137 | </dl> 138 | 139 | :param md: Markdown instance 140 | """ 141 | md.block.register("def_list", DEF_PATTERN, parse_def_list, before="paragraph") 142 | if md.renderer and md.renderer.NAME == "html": 143 | md.renderer.register("def_list", render_def_list) 144 | md.renderer.register("def_list_head", render_def_list_head) 145 | md.renderer.register("def_list_item", render_def_list_item) 146 | -------------------------------------------------------------------------------- /src/mistune/plugins/math.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Match 2 | 3 | if TYPE_CHECKING: 4 | from ..block_parser import BlockParser 5 | from ..core import BaseRenderer, BlockState, InlineState 6 | from ..inline_parser import InlineParser 7 | from ..markdown import Markdown 8 | 9 | __all__ = ["math", "math_in_quote", "math_in_list"] 10 | 11 | BLOCK_MATH_PATTERN = r"^ {0,3}\$\$[ \t]*\n(?P<math_text>[\s\S]+?)\n\$\$[ \t]*$" 12 | INLINE_MATH_PATTERN = r"\$(?!\s)(?P<math_text>.+?)(?!\s)\$" 13 | 14 | 15 | def parse_block_math(block: "BlockParser", m: Match[str], state: "BlockState") -> int: 16 | text = m.group("math_text") 17 | state.append_token({"type": "block_math", "raw": text}) 18 | return m.end() + 1 19 | 20 | 21 | def parse_inline_math(inline: "InlineParser", m: Match[str], state: "InlineState") -> int: 22 | text = m.group("math_text") 23 | state.append_token({"type": "inline_math", "raw": text}) 24 | return m.end() 25 | 26 | 27 | def render_block_math(renderer: "BaseRenderer", text: str) -> str: 28 | return '<div class="math">$$\n' + text + "\n$$</div>\n" 29 | 30 | 31 | def render_inline_math(renderer: "BaseRenderer", text: str) -> str: 32 | return r'<span class="math">\(' + text + r"\)</span>" 33 | 34 | 35 | def math(md: "Markdown") -> None: 36 | """A mistune plugin to support math. The syntax is used 37 | by many markdown extensions: 38 | 39 | .. code-block:: text 40 | 41 | Block math is surrounded by $$: 42 | 43 | $$ 44 | f(a)=f(b) 45 | $$ 46 | 47 | Inline math is surrounded by `$`, such as $f(a)=f(b)$ 48 | 49 | :param md: Markdown instance 50 | """ 51 | md.block.register("block_math", BLOCK_MATH_PATTERN, parse_block_math, before="list") 52 | md.inline.register("inline_math", INLINE_MATH_PATTERN, parse_inline_math, before="link") 53 | if md.renderer and md.renderer.NAME == "html": 54 | md.renderer.register("block_math", render_block_math) 55 | md.renderer.register("inline_math", render_inline_math) 56 | 57 | 58 | def math_in_quote(md: "Markdown") -> None: 59 | """Enable block math plugin in block quote.""" 60 | md.block.insert_rule(md.block.block_quote_rules, "block_math", before="list") 61 | 62 | 63 | def math_in_list(md: "Markdown") -> None: 64 | """Enable block math plugin in list.""" 65 | md.block.insert_rule(md.block.list_rules, "block_math", before="list") 66 | -------------------------------------------------------------------------------- /src/mistune/plugins/ruby.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, Any, Dict, List, Match, Optional 3 | 4 | from ..helpers import parse_link, parse_link_label 5 | from ..util import unikey 6 | 7 | if TYPE_CHECKING: 8 | from ..core import BaseRenderer, InlineState 9 | from ..inline_parser import InlineParser 10 | from ..markdown import Markdown 11 | 12 | 13 | RUBY_PATTERN = r"\[(?:\w+\([\w ]+\))+\]" 14 | _ruby_re = re.compile(RUBY_PATTERN) 15 | 16 | 17 | def parse_ruby(inline: "InlineParser", m: Match[str], state: "InlineState") -> int: 18 | text = m.group(0)[1:-2] 19 | items = text.split(")") 20 | tokens = [] 21 | for item in items: 22 | rb, rt = item.split("(") 23 | tokens.append({"type": "ruby", "raw": rb, "attrs": {"rt": rt}}) 24 | 25 | end_pos = m.end() 26 | 27 | next_match = _ruby_re.match(state.src, end_pos) 28 | if next_match: 29 | for tok in tokens: 30 | state.append_token(tok) 31 | return parse_ruby(inline, next_match, state) 32 | 33 | # repeat link logic 34 | if end_pos < len(state.src): 35 | link_pos = _parse_ruby_link(inline, state, end_pos, tokens) 36 | if link_pos: 37 | return link_pos 38 | 39 | for tok in tokens: 40 | state.append_token(tok) 41 | return end_pos 42 | 43 | 44 | def _parse_ruby_link( 45 | inline: "InlineParser", state: "InlineState", pos: int, tokens: List[Dict[str, Any]] 46 | ) -> Optional[int]: 47 | c = state.src[pos] 48 | if c == "(": 49 | # standard link [text](<url> "title") 50 | attrs, link_pos = parse_link(state.src, pos + 1) 51 | if link_pos: 52 | state.append_token( 53 | { 54 | "type": "link", 55 | "children": tokens, 56 | "attrs": attrs, 57 | } 58 | ) 59 | return link_pos 60 | 61 | elif c == "[": 62 | # standard ref link [text][label] 63 | label, link_pos = parse_link_label(state.src, pos + 1) 64 | if label and link_pos: 65 | ref_links = state.env["ref_links"] 66 | key = unikey(label) 67 | env = ref_links.get(key) 68 | if env: 69 | attrs = {"url": env["url"], "title": env.get("title")} 70 | state.append_token( 71 | { 72 | "type": "link", 73 | "children": tokens, 74 | "attrs": attrs, 75 | } 76 | ) 77 | else: 78 | for tok in tokens: 79 | state.append_token(tok) 80 | state.append_token( 81 | { 82 | "type": "text", 83 | "raw": "[" + label + "]", 84 | } 85 | ) 86 | return link_pos 87 | return None 88 | 89 | 90 | def render_ruby(renderer: "BaseRenderer", text: str, rt: str) -> str: 91 | return "<ruby>" + text + "<rt>" + rt + "</rt></ruby>" 92 | 93 | 94 | def ruby(md: "Markdown") -> None: 95 | """A mistune plugin to support ``<ruby>`` tag. The syntax is defined 96 | at https://lepture.com/en/2022/markdown-ruby-markup: 97 | 98 | .. code-block:: text 99 | 100 | [漢字(ㄏㄢˋㄗˋ)] 101 | [漢(ㄏㄢˋ)字(ㄗˋ)] 102 | 103 | [漢字(ㄏㄢˋㄗˋ)][link] 104 | [漢字(ㄏㄢˋㄗˋ)](/url "title") 105 | 106 | [link]: /url "title" 107 | 108 | :param md: Markdown instance 109 | """ 110 | md.inline.register("ruby", RUBY_PATTERN, parse_ruby, before="link") 111 | if md.renderer and md.renderer.NAME == "html": 112 | md.renderer.register("ruby", render_ruby) 113 | -------------------------------------------------------------------------------- /src/mistune/plugins/speedup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import string 3 | from typing import TYPE_CHECKING, Match 4 | 5 | if TYPE_CHECKING: 6 | from ..block_parser import BlockParser 7 | from ..core import BlockState, InlineState 8 | from ..inline_parser import InlineParser 9 | from ..markdown import Markdown 10 | 11 | # because mismatch is too slow, add parsers for paragraph and text 12 | 13 | HARD_LINEBREAK_RE = re.compile(r" *\n\s*") 14 | PARAGRAPH = ( 15 | # start with none punctuation, not number, not whitespace 16 | r"(?:^[^\s\d" + re.escape(string.punctuation) + r"][^\n]*\n)+" 17 | ) 18 | 19 | __all__ = ["speedup"] 20 | 21 | 22 | def parse_text(inline: "InlineParser", m: Match[str], state: "InlineState") -> int: 23 | text = m.group(0) 24 | text = HARD_LINEBREAK_RE.sub("\n", text) 25 | inline.process_text(text, state) 26 | return m.end() 27 | 28 | 29 | def parse_paragraph(block: "BlockParser", m: Match[str], state: "BlockState") -> int: 30 | text = m.group(0) 31 | state.add_paragraph(text) 32 | return m.end() 33 | 34 | 35 | def speedup(md: "Markdown") -> None: 36 | """Increase the speed of parsing paragraph and inline text.""" 37 | md.block.register("paragraph", PARAGRAPH, parse_paragraph) 38 | 39 | punc = r"\\><!\[_*`~\^\$=" 40 | text_pattern = r"[\s\S]+?(?=[" + punc + r"]|" 41 | if "url_link" in md.inline.rules: 42 | text_pattern += "https?:|" 43 | 44 | if md.inline.hard_wrap: 45 | text_pattern += r" *\n|" 46 | else: 47 | text_pattern += r" {2,}\n|" 48 | 49 | text_pattern += r"$)" 50 | md.inline.register("text", text_pattern, parse_text) 51 | -------------------------------------------------------------------------------- /src/mistune/plugins/spoiler.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, Match 3 | 4 | if TYPE_CHECKING: 5 | from ..block_parser import BlockParser 6 | from ..core import BaseRenderer, BlockState, InlineState 7 | from ..inline_parser import InlineParser 8 | from ..markdown import Markdown 9 | 10 | __all__ = ["spoiler"] 11 | 12 | _BLOCK_SPOILER_START = re.compile(r"^ {0,3}! ?", re.M) 13 | _BLOCK_SPOILER_MATCH = re.compile(r"^( {0,3}![^\n]*\n)+$") 14 | 15 | INLINE_SPOILER_PATTERN = r">!\s*(?P<spoiler_text>.+?)\s*!<" 16 | 17 | 18 | def parse_block_spoiler(block: "BlockParser", m: Match[str], state: "BlockState") -> int: 19 | text, end_pos = block.extract_block_quote(m, state) 20 | if not text.endswith("\n"): 21 | # ensure it endswith \n to make sure 22 | # _BLOCK_SPOILER_MATCH.match works 23 | text += "\n" 24 | 25 | depth = state.depth() 26 | if not depth and _BLOCK_SPOILER_MATCH.match(text): 27 | text = _BLOCK_SPOILER_START.sub("", text) 28 | tok_type = "block_spoiler" 29 | else: 30 | tok_type = "block_quote" 31 | 32 | # scan children state 33 | child = state.child_state(text) 34 | if state.depth() >= block.max_nested_level - 1: 35 | rules = list(block.block_quote_rules) 36 | rules.remove("block_quote") 37 | else: 38 | rules = block.block_quote_rules 39 | 40 | block.parse(child, rules) 41 | token = {"type": tok_type, "children": child.tokens} 42 | if end_pos: 43 | state.prepend_token(token) 44 | return end_pos 45 | state.append_token(token) 46 | return state.cursor 47 | 48 | 49 | def parse_inline_spoiler(inline: "InlineParser", m: Match[str], state: "InlineState") -> int: 50 | text = m.group("spoiler_text") 51 | new_state = state.copy() 52 | new_state.src = text 53 | children = inline.render(new_state) 54 | state.append_token({"type": "inline_spoiler", "children": children}) 55 | return m.end() 56 | 57 | 58 | def render_block_spoiler(renderer: "BaseRenderer", text: str) -> str: 59 | return '<div class="spoiler">\n' + text + "</div>\n" 60 | 61 | 62 | def render_inline_spoiler(renderer: "BaseRenderer", text: str) -> str: 63 | return '<span class="spoiler">' + text + "</span>" 64 | 65 | 66 | def spoiler(md: "Markdown") -> None: 67 | """A mistune plugin to support block and inline spoiler. The 68 | syntax is inspired by stackexchange: 69 | 70 | .. code-block:: text 71 | 72 | Block level spoiler looks like block quote, but with `>!`: 73 | 74 | >! this is spoiler 75 | >! 76 | >! the content will be hidden 77 | 78 | Inline spoiler is surrounded by `>!` and `!<`, such as >! hide me !<. 79 | 80 | :param md: Markdown instance 81 | """ 82 | # reset block quote parser with block spoiler parser 83 | md.block.register("block_quote", None, parse_block_spoiler) 84 | md.inline.register("inline_spoiler", INLINE_SPOILER_PATTERN, parse_inline_spoiler) 85 | if md.renderer and md.renderer.NAME == "html": 86 | md.renderer.register("block_spoiler", render_block_spoiler) 87 | md.renderer.register("inline_spoiler", render_inline_spoiler) 88 | -------------------------------------------------------------------------------- /src/mistune/plugins/task_lists.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, Any, Dict, Iterable 3 | 4 | if TYPE_CHECKING: 5 | from ..core import BaseRenderer, BlockState 6 | from ..markdown import Markdown 7 | 8 | __all__ = ["task_lists"] 9 | 10 | 11 | TASK_LIST_ITEM = re.compile(r"^(\[[ xX]\])\s+") 12 | 13 | 14 | def task_lists_hook(md: "Markdown", state: "BlockState") -> Iterable[Dict[str, Any]]: 15 | return _rewrite_all_list_items(state.tokens) 16 | 17 | 18 | def render_task_list_item(renderer: "BaseRenderer", text: str, checked: bool = False) -> str: 19 | checkbox = '<input class="task-list-item-checkbox" type="checkbox" disabled' 20 | if checked: 21 | checkbox += " checked/>" 22 | else: 23 | checkbox += "/>" 24 | 25 | if text.startswith("<p>"): 26 | text = text.replace("<p>", "<p>" + checkbox, 1) 27 | else: 28 | text = checkbox + text 29 | 30 | return '<li class="task-list-item">' + text + "</li>\n" 31 | 32 | 33 | def task_lists(md: "Markdown") -> None: 34 | """A mistune plugin to support task lists. Spec defined by 35 | GitHub flavored Markdown and commonly used by many parsers: 36 | 37 | .. code-block:: text 38 | 39 | - [ ] unchecked task 40 | - [x] checked task 41 | 42 | :param md: Markdown instance 43 | """ 44 | md.before_render_hooks.append(task_lists_hook) 45 | if md.renderer and md.renderer.NAME == "html": 46 | md.renderer.register("task_list_item", render_task_list_item) 47 | 48 | 49 | def _rewrite_all_list_items(tokens: Iterable[Dict[str, Any]]) -> Iterable[Dict[str, Any]]: 50 | for tok in tokens: 51 | if tok["type"] == "list_item": 52 | _rewrite_list_item(tok) 53 | if "children" in tok: 54 | _rewrite_all_list_items(tok["children"]) 55 | return tokens 56 | 57 | 58 | def _rewrite_list_item(tok: Dict[str, Any]) -> None: 59 | children = tok["children"] 60 | if children: 61 | first_child = children[0] 62 | text = first_child.get("text", "") 63 | m = TASK_LIST_ITEM.match(text) 64 | if m: 65 | mark = m.group(1) 66 | first_child["text"] = text[m.end() :] 67 | 68 | tok["type"] = "task_list_item" 69 | tok["attrs"] = {"checked": mark != "[ ]"} 70 | -------------------------------------------------------------------------------- /src/mistune/plugins/url.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Match 2 | 3 | from ..util import escape_url 4 | 5 | if TYPE_CHECKING: 6 | from ..core import InlineState 7 | from ..inline_parser import InlineParser 8 | from ..markdown import Markdown 9 | 10 | __all__ = ["url"] 11 | 12 | URL_LINK_PATTERN = r"""https?:\/\/[^\s<]+[^<.,:;"')\]\s]""" 13 | 14 | 15 | def parse_url_link(inline: "InlineParser", m: Match[str], state: "InlineState") -> int: 16 | text = m.group(0) 17 | pos = m.end() 18 | if state.in_link: 19 | inline.process_text(text, state) 20 | return pos 21 | state.append_token( 22 | { 23 | "type": "link", 24 | "children": [{"type": "text", "raw": text}], 25 | "attrs": {"url": escape_url(text)}, 26 | } 27 | ) 28 | return pos 29 | 30 | 31 | def url(md: "Markdown") -> None: 32 | md.inline.register("url_link", URL_LINK_PATTERN, parse_url_link) 33 | -------------------------------------------------------------------------------- /src/mistune/py.typed: -------------------------------------------------------------------------------- 1 | # when type checking dependents, tell type checkers, e.g. mypy, to use this package's types 2 | # without this file, mypy will write a warning and ignore type annotations from mistune. 3 | -------------------------------------------------------------------------------- /src/mistune/renderers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepture/mistune/e8d0a030ce2dd8e7a034fcf61c5f6a29ff677fdf/src/mistune/renderers/__init__.py -------------------------------------------------------------------------------- /src/mistune/renderers/_list.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Dict, Iterable, cast 2 | 3 | from ..util import strip_end 4 | 5 | if TYPE_CHECKING: 6 | from ..core import BaseRenderer, BlockState 7 | 8 | 9 | def render_list(renderer: "BaseRenderer", token: Dict[str, Any], state: "BlockState") -> str: 10 | attrs = token["attrs"] 11 | if attrs["ordered"]: 12 | children = _render_ordered_list(renderer, token, state) 13 | else: 14 | children = _render_unordered_list(renderer, token, state) 15 | 16 | text = "".join(children) 17 | parent = token.get("parent") 18 | if parent: 19 | if parent["tight"]: 20 | return text 21 | return text + "\n" 22 | return strip_end(text) + "\n" 23 | 24 | 25 | def _render_list_item( 26 | renderer: "BaseRenderer", 27 | parent: Dict[str, Any], 28 | item: Dict[str, Any], 29 | state: "BlockState", 30 | ) -> str: 31 | leading = cast(str, parent["leading"]) 32 | text = "" 33 | for tok in item["children"]: 34 | if tok["type"] == "list": 35 | tok["parent"] = parent 36 | elif tok["type"] == "blank_line": 37 | continue 38 | text += renderer.render_token(tok, state) 39 | 40 | lines = text.splitlines() 41 | text = (lines[0] if lines else "") + "\n" 42 | prefix = " " * len(leading) 43 | for line in lines[1:]: 44 | if line: 45 | text += prefix + line + "\n" 46 | else: 47 | text += "\n" 48 | return leading + text 49 | 50 | 51 | def _render_ordered_list(renderer: "BaseRenderer", token: Dict[str, Any], state: "BlockState") -> Iterable[str]: 52 | attrs = token["attrs"] 53 | start = attrs.get("start", 1) 54 | for item in token["children"]: 55 | leading = str(start) + token["bullet"] + " " 56 | parent = { 57 | "leading": leading, 58 | "tight": token["tight"], 59 | } 60 | yield _render_list_item(renderer, parent, item, state) 61 | start += 1 62 | 63 | 64 | def _render_unordered_list(renderer: "BaseRenderer", token: Dict[str, Any], state: "BlockState") -> Iterable[str]: 65 | parent = { 66 | "leading": token["bullet"] + " ", 67 | "tight": token["tight"], 68 | } 69 | for item in token["children"]: 70 | yield _render_list_item(renderer, parent, item, state) 71 | -------------------------------------------------------------------------------- /src/mistune/renderers/html.py: -------------------------------------------------------------------------------- 1 | from typing import Any, ClassVar, Dict, Optional, Tuple, Literal 2 | from ..core import BaseRenderer, BlockState 3 | from ..util import escape as escape_text 4 | from ..util import safe_entity, striptags 5 | 6 | 7 | class HTMLRenderer(BaseRenderer): 8 | """A renderer for converting Markdown to HTML.""" 9 | 10 | _escape: bool 11 | NAME: ClassVar[Literal["html"]] = "html" 12 | HARMFUL_PROTOCOLS: ClassVar[Tuple[str, ...]] = ( 13 | "javascript:", 14 | "vbscript:", 15 | "file:", 16 | "data:", 17 | ) 18 | GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = ( 19 | "data:image/gif;", 20 | "data:image/png;", 21 | "data:image/jpeg;", 22 | "data:image/webp;", 23 | ) 24 | 25 | def __init__(self, escape: bool = True, allow_harmful_protocols: Optional[bool] = None) -> None: 26 | super(HTMLRenderer, self).__init__() 27 | self._allow_harmful_protocols = allow_harmful_protocols 28 | self._escape = escape 29 | 30 | def render_token(self, token: Dict[str, Any], state: BlockState) -> str: 31 | # backward compitable with v2 32 | func = self._get_method(token["type"]) 33 | attrs = token.get("attrs") 34 | 35 | if "raw" in token: 36 | text = token["raw"] 37 | elif "children" in token: 38 | text = self.render_tokens(token["children"], state) 39 | else: 40 | if attrs: 41 | return func(**attrs) 42 | else: 43 | return func() 44 | if attrs: 45 | return func(text, **attrs) 46 | else: 47 | return func(text) 48 | 49 | def safe_url(self, url: str) -> str: 50 | """Ensure the given URL is safe. This method is used for rendering 51 | links, images, and etc. 52 | """ 53 | if self._allow_harmful_protocols is True: 54 | return escape_text(url) 55 | 56 | _url = url.lower() 57 | if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)): 58 | return escape_text(url) 59 | 60 | if _url.startswith(self.HARMFUL_PROTOCOLS) and not _url.startswith(self.GOOD_DATA_PROTOCOLS): 61 | return "#harmful-link" 62 | return escape_text(url) 63 | 64 | def text(self, text: str) -> str: 65 | if self._escape: 66 | return escape_text(text) 67 | return safe_entity(text) 68 | 69 | def emphasis(self, text: str) -> str: 70 | return "<em>" + text + "</em>" 71 | 72 | def strong(self, text: str) -> str: 73 | return "<strong>" + text + "</strong>" 74 | 75 | def link(self, text: str, url: str, title: Optional[str] = None) -> str: 76 | s = '<a href="' + self.safe_url(url) + '"' 77 | if title: 78 | s += ' title="' + safe_entity(title) + '"' 79 | return s + ">" + text + "</a>" 80 | 81 | def image(self, text: str, url: str, title: Optional[str] = None) -> str: 82 | src = self.safe_url(url) 83 | alt = escape_text(striptags(text)) 84 | s = '<img src="' + src + '" alt="' + alt + '"' 85 | if title: 86 | s += ' title="' + safe_entity(title) + '"' 87 | return s + " />" 88 | 89 | def codespan(self, text: str) -> str: 90 | return "<code>" + escape_text(text) + "</code>" 91 | 92 | def linebreak(self) -> str: 93 | return "<br />\n" 94 | 95 | def softbreak(self) -> str: 96 | return "\n" 97 | 98 | def inline_html(self, html: str) -> str: 99 | if self._escape: 100 | return escape_text(html) 101 | return html 102 | 103 | def paragraph(self, text: str) -> str: 104 | return "<p>" + text + "</p>\n" 105 | 106 | def heading(self, text: str, level: int, **attrs: Any) -> str: 107 | tag = "h" + str(level) 108 | html = "<" + tag 109 | _id = attrs.get("id") 110 | if _id: 111 | html += ' id="' + _id + '"' 112 | return html + ">" + text + "</" + tag + ">\n" 113 | 114 | def blank_line(self) -> str: 115 | return "" 116 | 117 | def thematic_break(self) -> str: 118 | return "<hr />\n" 119 | 120 | def block_text(self, text: str) -> str: 121 | return text 122 | 123 | def block_code(self, code: str, info: Optional[str] = None) -> str: 124 | html = "<pre><code" 125 | if info is not None: 126 | info = safe_entity(info.strip()) 127 | if info: 128 | lang = info.split(None, 1)[0] 129 | html += ' class="language-' + lang + '"' 130 | return html + ">" + escape_text(code) + "</code></pre>\n" 131 | 132 | def block_quote(self, text: str) -> str: 133 | return "<blockquote>\n" + text + "</blockquote>\n" 134 | 135 | def block_html(self, html: str) -> str: 136 | if self._escape: 137 | return "<p>" + escape_text(html.strip()) + "</p>\n" 138 | return html + "\n" 139 | 140 | def block_error(self, text: str) -> str: 141 | return '<div class="error"><pre>' + text + "</pre></div>\n" 142 | 143 | def list(self, text: str, ordered: bool, **attrs: Any) -> str: 144 | if ordered: 145 | html = "<ol" 146 | start = attrs.get("start") 147 | if start is not None: 148 | html += ' start="' + str(start) + '"' 149 | return html + ">\n" + text + "</ol>\n" 150 | return "<ul>\n" + text + "</ul>\n" 151 | 152 | def list_item(self, text: str) -> str: 153 | return "<li>" + text + "</li>\n" 154 | -------------------------------------------------------------------------------- /src/mistune/toc.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple 2 | 3 | from .core import BlockState 4 | from .util import striptags 5 | 6 | if TYPE_CHECKING: 7 | from .markdown import Markdown 8 | 9 | 10 | def add_toc_hook( 11 | md: "Markdown", 12 | min_level: int = 1, 13 | max_level: int = 3, 14 | heading_id: Optional[Callable[[Dict[str, Any], int], str]] = None, 15 | ) -> None: 16 | """Add a hook to save toc items into ``state.env``. This is 17 | usually helpful for doc generator:: 18 | 19 | import mistune 20 | from mistune.toc import add_toc_hook, render_toc_ul 21 | 22 | md = mistune.create_markdown(...) 23 | add_toc_hook(md) 24 | 25 | html, state = md.parse(text) 26 | toc_items = state.env['toc_items'] 27 | toc_html = render_toc_ul(toc_items) 28 | 29 | :param md: Markdown instance 30 | :param min_level: min heading level 31 | :param max_level: max heading level 32 | :param heading_id: a function to generate heading_id 33 | """ 34 | if heading_id is None: 35 | 36 | def heading_id(token: Dict[str, Any], index: int) -> str: 37 | return "toc_" + str(index + 1) 38 | 39 | def toc_hook(md: "Markdown", state: "BlockState") -> None: 40 | headings = [] 41 | 42 | for tok in state.tokens: 43 | if tok["type"] == "heading": 44 | level = tok["attrs"]["level"] 45 | if min_level <= level <= max_level: 46 | headings.append(tok) 47 | 48 | toc_items = [] 49 | for i, tok in enumerate(headings): 50 | tok["attrs"]["id"] = heading_id(tok, i) 51 | toc_items.append(normalize_toc_item(md, tok)) 52 | 53 | # save items into state 54 | state.env["toc_items"] = toc_items 55 | 56 | md.before_render_hooks.append(toc_hook) 57 | 58 | 59 | def normalize_toc_item(md: "Markdown", token: Dict[str, Any]) -> Tuple[int, str, str]: 60 | text = token["text"] 61 | tokens = md.inline(text, {}) 62 | assert md.renderer is not None 63 | html = md.renderer(tokens, BlockState()) 64 | text = striptags(html) 65 | attrs = token["attrs"] 66 | return attrs["level"], attrs["id"], text 67 | 68 | 69 | def render_toc_ul(toc: Iterable[Tuple[int, str, str]]) -> str: 70 | """Render a <ul> table of content HTML. The param "toc" should 71 | be formatted into this structure:: 72 | 73 | [ 74 | (level, id, text), 75 | ] 76 | 77 | For example:: 78 | 79 | [ 80 | (1, 'toc-intro', 'Introduction'), 81 | (2, 'toc-install', 'Install'), 82 | (2, 'toc-upgrade', 'Upgrade'), 83 | (1, 'toc-license', 'License'), 84 | ] 85 | """ 86 | if not toc: 87 | return "" 88 | 89 | s = "" 90 | levels: List[int] = [] 91 | for level, k, text in toc: 92 | item = '<a href="#{}">{}</a>'.format(k, text) 93 | if not levels: 94 | s += "<li>" + item 95 | levels.append(level) 96 | elif level == levels[-1]: 97 | s += "</li>\n<li>" + item 98 | elif level > levels[-1]: 99 | s += "\n<ul>\n<li>" + item 100 | levels.append(level) 101 | else: 102 | levels.pop() 103 | while levels: 104 | last_level = levels.pop() 105 | if level == last_level: 106 | s += "</li>\n</ul>\n</li>\n<li>" + item 107 | levels.append(level) 108 | break 109 | elif level > last_level: 110 | s += "</li>\n<li>" + item 111 | levels.append(last_level) 112 | levels.append(level) 113 | break 114 | else: 115 | s += "</li>\n</ul>\n" 116 | else: 117 | levels.append(level) 118 | s += "</li>\n<li>" + item 119 | 120 | while len(levels) > 1: 121 | s += "</li>\n</ul>\n" 122 | levels.pop() 123 | 124 | if not s: 125 | return "" 126 | return "<ul>\n" + s + "</li>\n</ul>\n" 127 | -------------------------------------------------------------------------------- /src/mistune/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | from html import _replace_charref # type: ignore[attr-defined] 3 | from typing import Match 4 | from urllib.parse import quote 5 | 6 | _expand_tab_re = re.compile(r"^( {0,3})\t", flags=re.M) 7 | 8 | 9 | def expand_leading_tab(text: str, width: int = 4) -> str: 10 | def repl(m: Match[str]) -> str: 11 | s = m.group(1) 12 | return s + " " * (width - len(s)) 13 | 14 | return _expand_tab_re.sub(repl, text) 15 | 16 | 17 | def expand_tab(text: str, space: str = " ") -> str: 18 | repl = r"\1" + space 19 | return _expand_tab_re.sub(repl, text) 20 | 21 | 22 | def escape(s: str, quote: bool = True) -> str: 23 | """Escape characters of ``&<>``. If quote=True, ``"`` will be 24 | converted to ``"e;``.""" 25 | s = s.replace("&", "&") 26 | s = s.replace("<", "<") 27 | s = s.replace(">", ">") 28 | if quote: 29 | s = s.replace('"', """) 30 | return s 31 | 32 | 33 | def escape_url(link: str) -> str: 34 | """Escape URL for safety.""" 35 | safe = ( 36 | ":/?#@" # gen-delims - '[]' (rfc3986) 37 | "!$&()*+,;=" # sub-delims - "'" (rfc3986) 38 | "%" # leave already-encoded octets alone 39 | ) 40 | return quote(unescape(link), safe=safe) 41 | 42 | 43 | def safe_entity(s: str) -> str: 44 | """Escape characters for safety.""" 45 | return escape(unescape(s)) 46 | 47 | 48 | def unikey(s: str) -> str: 49 | """Generate a unique key for links and footnotes.""" 50 | key = " ".join(s.split()).strip() 51 | return key.lower().upper() 52 | 53 | 54 | _charref_re = re.compile( 55 | r"&(#[0-9]{1,7};" 56 | r"|#[xX][0-9a-fA-F]+;" 57 | r"|[^\t\n\f <&#;]{1,32};)" 58 | ) 59 | 60 | 61 | def unescape(s: str) -> str: 62 | """ 63 | Copy from `html.unescape`, but `_charref` is different. CommonMark 64 | does not accept entity references without a trailing semicolon 65 | """ 66 | if "&" not in s: 67 | return s 68 | return _charref_re.sub(_replace_charref, s) 69 | 70 | 71 | _striptags_re = re.compile(r"(<!--.*?-->|<[^>]*>)") 72 | 73 | 74 | def striptags(s: str) -> str: 75 | return _striptags_re.sub("", s) 76 | 77 | 78 | _strip_end_re = re.compile(r"\n\s+$") 79 | 80 | 81 | def strip_end(src: str) -> str: 82 | return _strip_end_re.sub("\n", src) 83 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | from abc import abstractmethod 3 | from unittest import TestCase 4 | 5 | from tests import fixtures 6 | 7 | 8 | class BaseTestCase(TestCase): 9 | @classmethod 10 | def load_fixtures(cls, case_file: str) -> None: 11 | def attach_case(n: str, text: str, html: str) -> None: 12 | def method(self: "BaseTestCase") -> None: 13 | self.assert_case(n, text, html) 14 | 15 | name = "test_{}".format(n) 16 | method.__name__ = name 17 | method.__doc__ = "Run fixture {} - {}".format(case_file, n) 18 | setattr(cls, name, method) 19 | 20 | for n, text, html in fixtures.load_examples(case_file): 21 | if cls.ignore_case(n): 22 | continue 23 | attach_case(n, text, html) 24 | 25 | @classmethod 26 | def ignore_case(cls, name: str) -> bool: 27 | return False 28 | 29 | @abstractmethod 30 | def parse(self, text: str) -> str: ... 31 | 32 | def assert_case(self, name: str, text: str, html: str) -> None: 33 | result = self.parse(text) 34 | self.assertEqual(result, html) 35 | 36 | 37 | def normalize_html(html: str) -> str: 38 | html = re.sub(r">\n+", ">", html) 39 | html = re.sub(r"\n+<", "<", html) 40 | return html.strip() 41 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | from typing import Any, Iterable, Tuple 5 | 6 | ROOT = os.path.join(os.path.dirname(__file__)) 7 | 8 | EXAMPLE_PATTERN = re.compile( 9 | r"^`{32} example\n([\s\S]*?)" 10 | r"^\.\n([\s\S]*?)" 11 | r"^`{32}$|^#{1,6} *(.*)$", 12 | flags=re.M, 13 | ) 14 | 15 | 16 | def load_ast(filename: str) -> Any: 17 | with open(os.path.join(ROOT, "ast", filename)) as f: 18 | return json.load(f) 19 | 20 | 21 | def load_json(filename: str) -> Any: 22 | with open(os.path.join(ROOT, filename)) as f: 23 | return json.load(f) 24 | 25 | 26 | def load_examples(filename: str) -> Iterable[Tuple[str, str, str]]: 27 | if filename.endswith(".json"): 28 | data = load_json(filename) 29 | for item in data: 30 | section = item["section"].lower().replace(" ", "_") 31 | n = "%s_%03d" % (section, item["example"]) 32 | yield n, item["markdown"], item["html"] 33 | else: 34 | with open(os.path.join(ROOT, filename), "rb") as f: 35 | content = f.read() 36 | s = content.decode("utf-8") 37 | yield from parse_examples(s) 38 | 39 | 40 | def parse_examples(text: str) -> Iterable[Tuple[str, str, str]]: 41 | data = EXAMPLE_PATTERN.findall(text) 42 | 43 | section = None 44 | count = 0 45 | for md, html, title in data: 46 | if title: 47 | count = 0 48 | section = title.lower().replace(" ", "_") 49 | 50 | if md and html: 51 | count += 1 52 | n = "%s_%03d" % (section, count) 53 | md = md.replace("\u2192", "\t") 54 | html = html.replace("\u2192", "\t") 55 | yield n, md, html 56 | -------------------------------------------------------------------------------- /tests/fixtures/abbr.txt: -------------------------------------------------------------------------------- 1 | # Abbreviation 2 | 3 | ## abbreviation 4 | 5 | ```````````````````````````````` example 6 | HTML 7 | . 8 | <p>HTML</p> 9 | ```````````````````````````````` 10 | 11 | ```````````````````````````````` example 12 | HTML 13 | *[HTML]: Hyper Text Markup Language 14 | . 15 | <p><abbr title="Hyper Text Markup Language">HTML</abbr></p> 16 | ```````````````````````````````` 17 | 18 | ```````````````````````````````` example 19 | The HTML specification is maintained by the W3C. 20 | *[HTML]: Hyper Text Markup Language 21 | *[W3C]: World Wide Web Consortium 22 | . 23 | <p>The <abbr title="Hyper Text Markup Language">HTML</abbr> specification is maintained by the <abbr title="World Wide Web Consortium">W3C</abbr>.</p> 24 | ```````````````````````````````` 25 | 26 | ```````````````````````````````` example 27 | The HTML specification is maintained by the W3C. 28 | *[HTML]: 29 | Hyper Text Markup Language 30 | *[W3C]: 31 | World Wide Web Consortium 32 | . 33 | <p>The <abbr title="Hyper Text Markup Language">HTML</abbr> specification is maintained by the <abbr title="World Wide Web Consortium">W3C</abbr>.</p> 34 | ```````````````````````````````` 35 | 36 | ```````````````````````````````` example 37 | The HTML specification is maintained by the W3C. 38 | *[HTML]: Hyper Text Markup Language 39 | *[W3C]: World Wide Web Consortium 40 | There is more text below this one. 41 | . 42 | <p>The <abbr title="Hyper Text Markup Language">HTML</abbr> specification is maintained by the <abbr title="World Wide Web Consortium">W3C</abbr>.</p> 43 | <p>There is more text below this one.</p> 44 | ```````````````````````````````` 45 | 46 | ```````````````````````````````` example 47 | The latest spec of HTML is HTML5. 48 | *[HTML]: Hyper Text Markup Language 49 | . 50 | <p>The latest spec of <abbr title="Hyper Text Markup Language">HTML</abbr> is <abbr title="Hyper Text Markup Language">HTML</abbr>5.</p> 51 | ```````````````````````````````` 52 | 53 | ```````````````````````````````` example 54 | HTML should always be spelt with capital letters and not spelt html. 55 | *[HTML]: Hyper Text Markup Language 56 | . 57 | <p><abbr title="Hyper Text Markup Language">HTML</abbr> should always be spelt with capital letters and not spelt html.</p> 58 | ```````````````````````````````` 59 | 60 | ```````````````````````````````` example 61 | HTML is an abbreviation but nobody knows what it stands for. 62 | *[HTML]: 63 | . 64 | <p><abbr>HTML</abbr> is an abbreviation but nobody knows what it stands for.</p> 65 | ```````````````````````````````` 66 | 67 | ```````````````````````````````` example 68 | Foo.* looks like a regular expression. 69 | *[Foo.*]: Just foo 70 | . 71 | <p><abbr title="Just foo">Foo.*</abbr> looks like a regular expression.</p> 72 | ```````````````````````````````` 73 | 74 | ```````````````````````````````` example 75 | Untrusted HTML should be escaped. 76 | *[HTML]: <p>This is some HTML</p> 77 | . 78 | <p>Untrusted <abbr title="<p>This is some HTML</p>">HTML</abbr> should be escaped.</p> 79 | ```````````````````````````````` 80 | -------------------------------------------------------------------------------- /tests/fixtures/def_list.txt: -------------------------------------------------------------------------------- 1 | # Definition Lists 2 | 3 | ```````````````````````````````` example 4 | Term 5 | : definition 6 | . 7 | <dl> 8 | <dt>Term</dt> 9 | <dd>definition</dd> 10 | </dl> 11 | ```````````````````````````````` 12 | 13 | ```````````````````````````````` example 14 | Term 15 | : definition1 16 | : definition2 17 | . 18 | <dl> 19 | <dt>Term</dt> 20 | <dd>definition1</dd> 21 | <dd>definition2</dd> 22 | </dl> 23 | ```````````````````````````````` 24 | 25 | ```````````````````````````````` example 26 | Term 27 | : definition1 28 | : definition2 29 | 30 | paragraph 31 | . 32 | <dl> 33 | <dt>Term</dt> 34 | <dd>definition1</dd> 35 | <dd>definition2</dd> 36 | </dl> 37 | <p>paragraph</p> 38 | ```````````````````````````````` 39 | 40 | ```````````````````````````````` example 41 | First Term 42 | : This is the definition of the first term. 43 | 44 | Second Term 45 | : This is one definition of the second term. 46 | : This is another definition of the second term. 47 | . 48 | <dl> 49 | <dt>First Term</dt> 50 | <dd>This is the definition of the first term.</dd> 51 | <dt>Second Term</dt> 52 | <dd>This is one definition of the second term.</dd> 53 | <dd>This is another definition of the second term.</dd> 54 | </dl> 55 | ```````````````````````````````` 56 | 57 | ```````````````````````````````` example 58 | First Term 59 | : This is the definition of the first term. 60 | 61 | Second Term 62 | : This is one definition of the second term. 63 | : This is another definition of the second term. 64 | 65 | paragraph 66 | . 67 | <dl> 68 | <dt>First Term</dt> 69 | <dd>This is the definition of the first term.</dd> 70 | <dt>Second Term</dt> 71 | <dd>This is one definition of the second term.</dd> 72 | <dd>This is another definition of the second term.</dd> 73 | </dl> 74 | <p>paragraph</p> 75 | ```````````````````````````````` 76 | 77 | ```````````````````````````````` example 78 | First Term 79 | : This is the definition of the first term. 80 | Not Second Term 81 | : This is one definition of the second term. 82 | : This is another definition of the second term. 83 | 84 | paragraph 85 | . 86 | <dl> 87 | <dt>First Term</dt> 88 | <dd>This is the definition of the first term. 89 | Not Second Term</dd> 90 | <dd>This is one definition of the second term.</dd> 91 | <dd>This is another definition of the second term.</dd> 92 | </dl> 93 | <p>paragraph</p> 94 | ```````````````````````````````` 95 | 96 | ## Definition with Multiple Lines 97 | 98 | ```````````````````````````````` example 99 | Term 100 | 101 | : definition 102 | . 103 | <dl> 104 | <dt>Term</dt> 105 | <dd><p>definition</p> 106 | </dd> 107 | </dl> 108 | ```````````````````````````````` 109 | 110 | 111 | ```````````````````````````````` example 112 | Term 113 | : foo 114 | 115 | bar 116 | . 117 | <dl> 118 | <dt>Term</dt> 119 | <dd><p>foo</p> 120 | <p>bar</p> 121 | </dd> 122 | </dl> 123 | ```````````````````````````````` 124 | 125 | Leading 4 white spaces 126 | 127 | ```````````````````````````````` example 128 | Term 129 | : foo 130 | 131 | bar 132 | . 133 | <dl> 134 | <dt>Term</dt> 135 | <dd><p>foo</p> 136 | <p>bar</p> 137 | </dd> 138 | </dl> 139 | ```````````````````````````````` 140 | 141 | 142 | ```````````````````````````````` example 143 | Term 144 | : foo 145 | 146 | bar 147 | . 148 | <dl> 149 | <dt>Term</dt> 150 | <dd><p>foo</p> 151 | <pre><code>bar</code></pre> 152 | </dd> 153 | </dl> 154 | ```````````````````````````````` 155 | 156 | ```````````````````````````````` example 157 | Term 158 | : foo 159 | 160 | - bar 161 | - baz 162 | . 163 | <dl> 164 | <dt>Term</dt> 165 | <dd><p>foo</p> 166 | <ul> 167 | <li>bar</li> 168 | <li>baz</li> 169 | </ul> 170 | </dd> 171 | </dl> 172 | ```````````````````````````````` 173 | -------------------------------------------------------------------------------- /tests/fixtures/diff-commonmark.txt: -------------------------------------------------------------------------------- 1 | # Differences 2 | 3 | Results different than commonmark. 4 | 5 | 6 | ## Setext headings 7 | 8 | 9 | Example: 93 10 | 11 | 12 | ```````````````````````````````` example 13 | > foo 14 | bar 15 | === 16 | . 17 | <blockquote> 18 | <h1>foo 19 | bar</h1> 20 | </blockquote> 21 | ```````````````````````````````` 22 | 23 | If the dash is less than 3, it is not a `<hr>`, in this case, it 24 | can be rendered into `<h2>`. 25 | 26 | ```````````````````````````````` example 27 | - # Foo 28 | - Bar 29 | -- 30 | baz 31 | . 32 | <ul> 33 | <li> 34 | <h1>Foo</h1> 35 | </li> 36 | <li> 37 | <h2>Bar</h2> 38 | baz</li> 39 | </ul> 40 | ```````````````````````````````` 41 | 42 | ## Image 43 | 44 | Example 573 45 | 46 | ```````````````````````````````` example 47 | ![foo ![bar](/url)](/url2) 48 | . 49 | <p><img src="/url2" alt="foo ![bar](/url)" /></p> 50 | ```````````````````````````````` 51 | 52 | ## Link 53 | 54 | Example 517 55 | 56 | ```````````````````````````````` example 57 | [foo [bar](/uri)](/uri) 58 | . 59 | <p><a href="/uri">foo [bar](/uri)</a></p> 60 | ```````````````````````````````` 61 | 62 | Example 518 63 | 64 | ```````````````````````````````` example 65 | [foo *[bar [baz](/uri)](/uri)*](/uri) 66 | . 67 | <p><a href="/uri">foo <em>[bar [baz](/uri)](/uri)</em></a></p> 68 | ```````````````````````````````` 69 | 70 | Example 519 71 | 72 | ```````````````````````````````` example 73 | ![[[foo](uri1)](uri2)](uri3) 74 | . 75 | <p><img src="uri3" alt="[foo](uri1)" /></p> 76 | ```````````````````````````````` 77 | -------------------------------------------------------------------------------- /tests/fixtures/fenced_admonition.txt: -------------------------------------------------------------------------------- 1 | # Admonition 2 | 3 | ```````````````````````````````` example 4 | ```{unknown} Hi 5 | 6 | this is **strong** world 7 | ``` 8 | . 9 | <div class="error"><pre>{unknown} Hi 10 | 11 | this is **strong** world 12 | </pre></div> 13 | ```````````````````````````````` 14 | 15 | ```````````````````````````````` example 16 | ``` 17 | this is code 18 | ``` 19 | . 20 | <pre><code>this is code 21 | </code></pre> 22 | ```````````````````````````````` 23 | 24 | 25 | ```````````````````````````````` example 26 | ~~~{note} Warning 27 | 28 | message 29 | ~~~ 30 | . 31 | <section class="admonition note"> 32 | <p class="admonition-title">Warning</p> 33 | <p>message</p> 34 | </section> 35 | ```````````````````````````````` 36 | 37 | ```````````````````````````````` example 38 | ~~~{note} A **strong** title 39 | 40 | _message_ 41 | ~~~ 42 | . 43 | <section class="admonition note"> 44 | <p class="admonition-title">A <strong>strong</strong> title</p> 45 | <p><em>message</em></p> 46 | </section> 47 | ```````````````````````````````` 48 | 49 | Test with code block. 50 | 51 | ```````````````````````````````` example 52 | ```{note} 53 | 54 | print() 55 | ``` 56 | . 57 | <section class="admonition note"> 58 | <p class="admonition-title">Note</p> 59 | <pre><code>print()</code></pre> 60 | </section> 61 | ```````````````````````````````` 62 | 63 | Test with no message. 64 | 65 | ```````````````````````````````` example 66 | ~~~{note} Warning 67 | ~~~ 68 | . 69 | <section class="admonition note"> 70 | <p class="admonition-title">Warning</p> 71 | </section> 72 | ```````````````````````````````` 73 | 74 | Test with no ending fence 75 | 76 | ```````````````````````````````` example 77 | ~~~{note} Warning 78 | . 79 | <section class="admonition note"> 80 | <p class="admonition-title">Warning</p> 81 | </section> 82 | ```````````````````````````````` 83 | 84 | Test with nested admonition 85 | 86 | ```````````````````````````````` example 87 | ~~~~~~~{note} 88 | ~~~~~~{warning} 89 | ~~~~~{tip} 90 | ~~~~{caution} 91 | ~~~{hint} 92 | ``````{danger} 93 | `````{attention} 94 | ````{important} 95 | ```{error} 96 | ``` 97 | ```` 98 | ````` 99 | `````` 100 | ~~~ 101 | ~~~~ 102 | ~~~~~ 103 | ~~~~~~ 104 | ~~~~~~~ 105 | . 106 | <section class="admonition note"> 107 | <p class="admonition-title">Note</p> 108 | <section class="admonition warning"> 109 | <p class="admonition-title">Warning</p> 110 | <section class="admonition tip"> 111 | <p class="admonition-title">Tip</p> 112 | <section class="admonition caution"> 113 | <p class="admonition-title">Caution</p> 114 | <section class="admonition hint"> 115 | <p class="admonition-title">Hint</p> 116 | <section class="admonition danger"> 117 | <p class="admonition-title">Danger</p> 118 | <pre><code class="language-{attention}">````{important} 119 | ```{error} 120 | ``` 121 | ```` 122 | </code></pre> 123 | </section> 124 | </section> 125 | </section> 126 | </section> 127 | </section> 128 | </section> 129 | ```````````````````````````````` 130 | -------------------------------------------------------------------------------- /tests/fixtures/fenced_figure.txt: -------------------------------------------------------------------------------- 1 | # Figure 2 | 3 | ## No options 4 | 5 | ```````````````````````````````` example 6 | ~~~{figure} picture.png 7 | ~~~ 8 | . 9 | <figure class="figure"> 10 | <div class="block-image"><img src="picture.png" /></div> 11 | </figure> 12 | ```````````````````````````````` 13 | 14 | ## Align options 15 | 16 | ```````````````````````````````` example 17 | ~~~{figure} picture.png 18 | :align: left 19 | ~~~ 20 | . 21 | <figure class="figure align-left"> 22 | <div class="block-image"><img src="picture.png" /></div> 23 | </figure> 24 | ```````````````````````````````` 25 | 26 | 27 | ```````````````````````````````` example 28 | ~~~{figure} picture.png 29 | :align: invalid 30 | ~~~ 31 | . 32 | <figure class="figure"> 33 | <div class="block-image"><img src="picture.png" /></div> 34 | </figure> 35 | ```````````````````````````````` 36 | 37 | ## width and height 38 | 39 | ```````````````````````````````` example 40 | ~~~{figure} picture.png 41 | :width: 100 42 | :height: 50 43 | ~~~ 44 | . 45 | <figure class="figure"> 46 | <div class="block-image"><img src="picture.png" width="100" height="50" /></div> 47 | </figure> 48 | ```````````````````````````````` 49 | 50 | ```````````````````````````````` example 51 | ~~~{figure} picture.png 52 | :width: 100px 53 | :height: 50px 54 | ~~~ 55 | . 56 | <figure class="figure"> 57 | <div class="block-image"><img src="picture.png" style="width:100px;height:50px;" /></div> 58 | </figure> 59 | ```````````````````````````````` 60 | 61 | ```````````````````````````````` example 62 | ~~~{figure} picture.png 63 | :figwidth: 400px 64 | ~~~ 65 | . 66 | <figure class="figure" style="width:400px"> 67 | <div class="block-image"><img src="picture.png" /></div> 68 | </figure> 69 | ```````````````````````````````` 70 | 71 | ## alt option 72 | 73 | ```````````````````````````````` example 74 | ~~~{figure} picture.png 75 | :alt: description text 76 | ~~~ 77 | . 78 | <figure class="figure"> 79 | <div class="block-image"><img src="picture.png" alt="description text" /></div> 80 | </figure> 81 | ```````````````````````````````` 82 | 83 | ## target option 84 | 85 | ```````````````````````````````` example 86 | ~~~{figure} picture.png 87 | :target: https://lepture.com 88 | ~~~ 89 | . 90 | <figure class="figure"> 91 | <a class="block-image" href="https://lepture.com"><img src="picture.png" /></a> 92 | </figure> 93 | ```````````````````````````````` 94 | 95 | ## caption 96 | 97 | ```````````````````````````````` example 98 | ~~~{figure} picture.png 99 | a caption with **emphasis** text 100 | ~~~ 101 | . 102 | <figure class="figure"> 103 | <div class="block-image"><img src="picture.png" /></div> 104 | <figcaption>a caption with <strong>emphasis</strong> text</figcaption> 105 | </figure> 106 | ```````````````````````````````` 107 | 108 | ## legend 109 | 110 | ```````````````````````````````` example 111 | ~~~{figure} picture.png 112 | a caption with **emphasis** text 113 | 114 | > quote 115 | ~~~ 116 | . 117 | <figure class="figure"> 118 | <div class="block-image"><img src="picture.png" /></div> 119 | <figcaption>a caption with <strong>emphasis</strong> text</figcaption> 120 | <div class="legend"> 121 | <blockquote> 122 | <p>quote</p> 123 | </blockquote> 124 | </div> 125 | </figure> 126 | ```````````````````````````````` 127 | 128 | 129 | ```````````````````````````````` example 130 | ~~~{figure} picture.png 131 | > quote 132 | ~~~ 133 | . 134 | <figure class="figure"> 135 | <div class="block-image"><img src="picture.png" /></div> 136 | </figure> 137 | ```````````````````````````````` 138 | 139 | ## all options 140 | 141 | 142 | ```````````````````````````````` example 143 | ~~~{figure} picture.png 144 | :figwidth: 400px 145 | :figclass: all-options 146 | :target: https://lepture.com 147 | :alt: description 148 | :align: left 149 | :width: 100 150 | :height: 50 151 | 152 | a caption with **emphasis** text 153 | 154 | > quote 155 | ~~~ 156 | . 157 | <figure class="figure align-left all-options" style="width:400px"> 158 | <a class="block-image" href="https://lepture.com"><img src="picture.png" alt="description" width="100" height="50" /></a> 159 | <figcaption>a caption with <strong>emphasis</strong> text</figcaption> 160 | <div class="legend"> 161 | <blockquote> 162 | <p>quote</p> 163 | </blockquote> 164 | </div> 165 | </figure> 166 | ```````````````````````````````` 167 | -------------------------------------------------------------------------------- /tests/fixtures/fenced_image.txt: -------------------------------------------------------------------------------- 1 | # Image 2 | 3 | ## No options 4 | 5 | ```````````````````````````````` example 6 | ~~~{image} picture.png 7 | ~~~ 8 | . 9 | <div class="block-image"><img src="picture.png" /></div> 10 | ```````````````````````````````` 11 | 12 | ## Align options 13 | 14 | ```````````````````````````````` example 15 | ~~~{image} picture.png 16 | :align: left 17 | ~~~ 18 | . 19 | <div class="block-image align-left"><img src="picture.png" /></div> 20 | ```````````````````````````````` 21 | 22 | 23 | ```````````````````````````````` example 24 | ~~~{image} picture.png 25 | :align: invalid 26 | ~~~ 27 | . 28 | <div class="block-image"><img src="picture.png" /></div> 29 | ```````````````````````````````` 30 | 31 | ## width and height 32 | 33 | ```````````````````````````````` example 34 | ~~~{image} picture.png 35 | :width: 100 36 | :height: 50 37 | ~~~ 38 | . 39 | <div class="block-image"><img src="picture.png" width="100" height="50" /></div> 40 | ```````````````````````````````` 41 | 42 | ```````````````````````````````` example 43 | ~~~{image} picture.png 44 | :width: 100px 45 | :height: 50px 46 | ~~~ 47 | . 48 | <div class="block-image"><img src="picture.png" style="width:100px;height:50px;" /></div> 49 | ```````````````````````````````` 50 | 51 | ## alt option 52 | 53 | ```````````````````````````````` example 54 | ~~~{image} picture.png 55 | :alt: description text 56 | ~~~ 57 | . 58 | <div class="block-image"><img src="picture.png" alt="description text" /></div> 59 | ```````````````````````````````` 60 | 61 | ## target option 62 | 63 | ```````````````````````````````` example 64 | ~~~{image} picture.png 65 | :target: https://lepture.com 66 | ~~~ 67 | . 68 | <a class="block-image" href="https://lepture.com"><img src="picture.png" /></a> 69 | ```````````````````````````````` 70 | 71 | ## all options 72 | 73 | ```````````````````````````````` example 74 | ~~~{image} picture.png 75 | :target: https://lepture.com 76 | :alt: description 77 | :align: left 78 | :width: 100 79 | :height: 50 80 | ~~~ 81 | . 82 | <a class="block-image align-left" href="https://lepture.com"><img src="picture.png" alt="description" width="100" height="50" /></a> 83 | ```````````````````````````````` 84 | 85 | ## ampersand in source 86 | 87 | ```````````````````````````````` example 88 | ~~~{image} https://example.com/picture.png?foo=qux&test=me 89 | ~~~ 90 | . 91 | <div class="block-image"><img src="https://example.com/picture.png?foo=qux&test=me" /></div> 92 | ```````````````````````````````` 93 | 94 | ## ampersand in target 95 | 96 | ```````````````````````````````` example 97 | ~~~{image} picture.png 98 | :target: https://example.com/rickroll?a=1&b=2 99 | ~~~ 100 | . 101 | <a class="block-image" href="https://example.com/rickroll?a=1&b=2"><img src="picture.png" /></a> 102 | ```````````````````````````````` 103 | -------------------------------------------------------------------------------- /tests/fixtures/fenced_toc.txt: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | ## No TOC 4 | 5 | ```````````````````````````````` example 6 | none 7 | . 8 | <p>none</p> 9 | ```````````````````````````````` 10 | 11 | ```````````````````````````````` example 12 | # H1 13 | ## H2 14 | . 15 | <h1>H1</h1> 16 | <h2>H2</h2> 17 | ```````````````````````````````` 18 | 19 | 20 | ## Simple TOC 21 | 22 | ```````````````````````````````` example 23 | ~~~{toc} 24 | ~~~ 25 | 26 | # H1 27 | ## H2 28 | . 29 | <details class="toc" open> 30 | <summary>Table of Contents</summary> 31 | <ul> 32 | <li><a href="#toc_1">H1</a> 33 | <ul> 34 | <li><a href="#toc_2">H2</a></li> 35 | </ul> 36 | </li> 37 | </ul> 38 | </details> 39 | <h1 id="toc_1">H1</h1> 40 | <h2 id="toc_2">H2</h2> 41 | ```````````````````````````````` 42 | 43 | ## Invalid Option 44 | 45 | ```````````````````````````````` example 46 | # H1 47 | ## H2 48 | 49 | ~~~{toc} 50 | :max-level: s 51 | ~~~ 52 | . 53 | <h1>H1</h1> 54 | <h2>H2</h2> 55 | <div class="error"><pre>"max-level" option MUST be integer</pre></div> 56 | ```````````````````````````````` 57 | 58 | ```````````````````````````````` example 59 | # H1 60 | ## H2 61 | 62 | ~~~{toc} 63 | :max-level: 9 64 | ~~~ 65 | . 66 | <h1>H1</h1> 67 | <h2>H2</h2> 68 | <div class="error"><pre>"max-level" option MUST be <= 3</pre></div> 69 | ```````````````````````````````` 70 | 71 | ```````````````````````````````` example 72 | # H1 73 | ## H2 74 | 75 | ~~~{toc} 76 | :min-level: 2 77 | :max-level: 1 78 | ~~~ 79 | . 80 | <h1>H1</h1> 81 | <h2>H2</h2> 82 | <div class="error"><pre>"min-level" option MUST be less than "max-level" option</pre></div> 83 | ```````````````````````````````` 84 | 85 | ## Complex 86 | 87 | ```````````````````````````````` example 88 | # H1 89 | ## H2 90 | ### H3 91 | #### H4 92 | # H1 B 93 | # H1 `C` 94 | 95 | ```{toc} Contents 96 | ``` 97 | . 98 | <h1 id="toc_1">H1</h1> 99 | <h2 id="toc_2">H2</h2> 100 | <h3 id="toc_3">H3</h3> 101 | <h4 id="toc_4">H4</h4> 102 | <h1 id="toc_5">H1 B</h1> 103 | <h1 id="toc_6">H1 <code>C</code></h1> 104 | <details class="toc" open> 105 | <summary>Contents</summary> 106 | <ul> 107 | <li><a href="#toc_1">H1</a> 108 | <ul> 109 | <li><a href="#toc_2">H2</a> 110 | <ul> 111 | <li><a href="#toc_3">H3</a></li> 112 | </ul> 113 | </li> 114 | </ul> 115 | </li> 116 | <li><a href="#toc_5">H1 B</a></li> 117 | <li><a href="#toc_6">H1 C</a></li> 118 | </ul> 119 | </details> 120 | ```````````````````````````````` 121 | 122 | ## Insane 123 | 124 | ```````````````````````````````` example 125 | # H1 126 | ### H3 127 | ## H2 128 | #### H4 129 | ### H3 B 130 | # H1 B 131 | 132 | ```{toc} 133 | ``` 134 | . 135 | <h1 id="toc_1">H1</h1> 136 | <h3 id="toc_2">H3</h3> 137 | <h2 id="toc_3">H2</h2> 138 | <h4 id="toc_4">H4</h4> 139 | <h3 id="toc_5">H3 B</h3> 140 | <h1 id="toc_6">H1 B</h1> 141 | <details class="toc" open> 142 | <summary>Table of Contents</summary> 143 | <ul> 144 | <li><a href="#toc_1">H1</a> 145 | <ul> 146 | <li><a href="#toc_2">H3</a></li> 147 | <li><a href="#toc_3">H2</a> 148 | <ul> 149 | <li><a href="#toc_5">H3 B</a></li> 150 | </ul> 151 | </li> 152 | </ul> 153 | </li> 154 | <li><a href="#toc_6">H1 B</a></li> 155 | </ul> 156 | </details> 157 | ```````````````````````````````` 158 | 159 | ```````````````````````````````` example 160 | ### H3 161 | ## H2 162 | # H1 163 | 164 | ```{toc} 165 | ``` 166 | . 167 | <h3 id="toc_1">H3</h3> 168 | <h2 id="toc_2">H2</h2> 169 | <h1 id="toc_3">H1</h1> 170 | <details class="toc" open> 171 | <summary>Table of Contents</summary> 172 | <ul> 173 | <li><a href="#toc_1">H3</a></li> 174 | <li><a href="#toc_2">H2</a></li> 175 | <li><a href="#toc_3">H1</a></li> 176 | </ul> 177 | </details> 178 | ```````````````````````````````` 179 | 180 | ## Link in Heading 181 | 182 | 183 | ```````````````````````````````` example 184 | # [foo](/bar) 185 | 186 | ```{toc} 187 | ``` 188 | . 189 | <h1 id="toc_1"><a href="/bar">foo</a></h1> 190 | <details class="toc" open> 191 | <summary>Table of Contents</summary> 192 | <ul> 193 | <li><a href="#toc_1">foo</a></li> 194 | </ul> 195 | </details> 196 | ```````````````````````````````` 197 | 198 | ## HTML in Heading 199 | 200 | ```````````````````````````````` example 201 | # <em>H1</em> 202 | ```{toc} 203 | ``` 204 | . 205 | <h1 id="toc_1"><em>H1</em></h1> 206 | <details class="toc" open> 207 | <summary>Table of Contents</summary> 208 | <ul> 209 | <li><a href="#toc_1">H1</a></li> 210 | </ul> 211 | </details> 212 | ```````````````````````````````` 213 | 214 | ## Collapse TOC 215 | 216 | ```````````````````````````````` example 217 | 218 | ```{toc} 219 | :collapse: 220 | ``` 221 | 222 | # H1 223 | ## H2 224 | . 225 | <details class="toc"> 226 | <summary>Table of Contents</summary> 227 | <ul> 228 | <li><a href="#toc_1">H1</a> 229 | <ul> 230 | <li><a href="#toc_2">H2</a></li> 231 | </ul> 232 | </li> 233 | </ul> 234 | </details> 235 | <h1 id="toc_1">H1</h1> 236 | <h2 id="toc_2">H2</h2> 237 | ```````````````````````````````` 238 | -------------------------------------------------------------------------------- /tests/fixtures/fix-commonmark.txt: -------------------------------------------------------------------------------- 1 | Fix problems that commonmark has. 2 | 3 | 4 | ## Links 5 | 6 | Links can't contain links. 7 | 8 | ```````````````````````````````` example 9 | [<https://example.com>](/foo) 10 | . 11 | <p><a href="/foo"><https://example.com></a></p> 12 | ```````````````````````````````` 13 | 14 | ```````````````````````````````` example 15 | [<name@example.com>](/foo) 16 | . 17 | <p><a href="/foo"><name@example.com></a></p> 18 | ```````````````````````````````` 19 | 20 | ## Emphasis 21 | 22 | `<em>` doesn't contain `<em>`, `<strong>` doesn't contain `<strong>`. 23 | 24 | ```````````````````````````````` example 25 | *_em_* __**strong**__ ______m______ 26 | . 27 | <p><em>_em_</em> <strong>**strong**</strong> ______m______</p> 28 | ```````````````````````````````` 29 | 30 | ### Non aggressive emphasis 31 | 32 | ```````````````````````````````` example 33 | *a **b c* d** 34 | . 35 | <p><em>a **b c</em> d**</p> 36 | ```````````````````````````````` 37 | 38 | While CommonMark would render it into: 39 | 40 | ``` 41 | <p><em>a <em><em>b c</em> d</em></em></p> 42 | ``` 43 | 44 | ```````````````````````````````` example 45 | *a **b c* d** 46 | . 47 | <p><em>a **b c</em> d**</p> 48 | ```````````````````````````````` 49 | 50 | While CommonMark would render it into: 51 | 52 | ``` 53 | <p><em>a <em><em>b c</em> d</em></em></p> 54 | ``` 55 | 56 | What if the string is: 57 | 58 | ```````````````````````````````` example 59 | *a **b c* d 60 | . 61 | <p><em>a **b c</em> d</p> 62 | ```````````````````````````````` 63 | 64 | CommonMark would still be a mess: 65 | 66 | ``` 67 | <p>*a *<em>b c</em> d</p> 68 | ``` 69 | 70 | 71 | ## Max depth 72 | 73 | 74 | ```````````````````````````````` example 75 | > > > > > > > > b 76 | . 77 | <blockquote> 78 | <blockquote> 79 | <blockquote> 80 | <blockquote> 81 | <blockquote> 82 | <blockquote> 83 | <p>> > b</p> 84 | </blockquote> 85 | </blockquote> 86 | </blockquote> 87 | </blockquote> 88 | </blockquote> 89 | </blockquote> 90 | ```````````````````````````````` 91 | 92 | 93 | ```````````````````````````````` example 94 | - a 95 | - b 96 | - c 97 | - d 98 | - e 99 | - f 100 | - g 101 | - h 102 | . 103 | <ul> 104 | <li>a<ul> 105 | <li>b<ul> 106 | <li>c<ul> 107 | <li>d<ul> 108 | <li>e<ul> 109 | <li>f 110 | - g 111 | - h</li> 112 | </ul> 113 | </li> 114 | </ul> 115 | </li> 116 | </ul> 117 | </li> 118 | </ul> 119 | </li> 120 | </ul> 121 | </li> 122 | </ul> 123 | ```````````````````````````````` 124 | -------------------------------------------------------------------------------- /tests/fixtures/footnotes.txt: -------------------------------------------------------------------------------- 1 | # Footnotes 2 | 3 | ```````````````````````````````` example 4 | none [^1] 5 | . 6 | <p>none [^1]</p> 7 | ```````````````````````````````` 8 | 9 | ```````````````````````````````` example 10 | oneline [^1] 11 | 12 | [^1]: foo 13 | . 14 | <p>oneline <sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup></p> 15 | <section class="footnotes"> 16 | <ol> 17 | <li id="fn-1"><p>foo<a href="#fnref-1" class="footnote">↩</a></p></li> 18 | </ol> 19 | </section> 20 | ```````````````````````````````` 21 | 22 | ```````````````````````````````` example 23 | multiple lines [^1] 24 | 25 | [^1]: foo 26 | 27 | one 28 | 29 | two 30 | 31 | three 32 | 33 | four 34 | . 35 | <p>multiple lines <sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup></p> 36 | <pre><code>four</code></pre> 37 | <section class="footnotes"> 38 | <ol> 39 | <li id="fn-1"><p>foo</p> 40 | <p>one</p> 41 | <p>two</p> 42 | <p>three<a href="#fnref-1" class="footnote">↩</a></p></li> 43 | </ol> 44 | </section> 45 | ```````````````````````````````` 46 | -------------------------------------------------------------------------------- /tests/fixtures/hook_toc.txt: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | ## Simple TOC 4 | 5 | ```````````````````````````````` example 6 | # H1 7 | 8 | content 9 | 10 | ## H2 11 | . 12 | <h1 id="toc_1">H1</h1> 13 | <p>content</p> 14 | <h2 id="toc_2">H2</h2> 15 | <ul> 16 | <li><a href="#toc_1">H1</a> 17 | <ul> 18 | <li><a href="#toc_2">H2</a></li> 19 | </ul> 20 | </li> 21 | </ul> 22 | ```````````````````````````````` 23 | 24 | ## Complex TOC 25 | 26 | ```````````````````````````````` example 27 | # H1 28 | ## H2 29 | ### H3 30 | #### H4 31 | # H1 B 32 | # H1 `C` 33 | . 34 | <h1 id="toc_1">H1</h1> 35 | <h2 id="toc_2">H2</h2> 36 | <h3 id="toc_3">H3</h3> 37 | <h4>H4</h4> 38 | <h1 id="toc_4">H1 B</h1> 39 | <h1 id="toc_5">H1 <code>C</code></h1> 40 | <ul> 41 | <li><a href="#toc_1">H1</a> 42 | <ul> 43 | <li><a href="#toc_2">H2</a> 44 | <ul> 45 | <li><a href="#toc_3">H3</a></li> 46 | </ul> 47 | </li> 48 | </ul> 49 | </li> 50 | <li><a href="#toc_4">H1 B</a></li> 51 | <li><a href="#toc_5">H1 C</a></li> 52 | </ul> 53 | ```````````````````````````````` 54 | 55 | ## Insane TOC 56 | 57 | ```````````````````````````````` example 58 | # H1 59 | ### H3 60 | ## H2 61 | #### H4 62 | ### H3 B 63 | # H1 B 64 | . 65 | <h1 id="toc_1">H1</h1> 66 | <h3 id="toc_2">H3</h3> 67 | <h2 id="toc_3">H2</h2> 68 | <h4>H4</h4> 69 | <h3 id="toc_4">H3 B</h3> 70 | <h1 id="toc_5">H1 B</h1> 71 | <ul> 72 | <li><a href="#toc_1">H1</a> 73 | <ul> 74 | <li><a href="#toc_2">H3</a></li> 75 | <li><a href="#toc_3">H2</a> 76 | <ul> 77 | <li><a href="#toc_4">H3 B</a></li> 78 | </ul> 79 | </li> 80 | </ul> 81 | </li> 82 | <li><a href="#toc_5">H1 B</a></li> 83 | </ul> 84 | ```````````````````````````````` 85 | 86 | ```````````````````````````````` example 87 | ### H3 88 | ## H2 89 | # H1 90 | . 91 | <h3 id="toc_1">H3</h3> 92 | <h2 id="toc_2">H2</h2> 93 | <h1 id="toc_3">H1</h1> 94 | <ul> 95 | <li><a href="#toc_1">H3</a></li> 96 | <li><a href="#toc_2">H2</a></li> 97 | <li><a href="#toc_3">H1</a></li> 98 | </ul> 99 | ```````````````````````````````` 100 | 101 | ## Link in Heading 102 | 103 | 104 | ```````````````````````````````` example 105 | # [foo](/bar) 106 | . 107 | <h1 id="toc_1"><a href="/bar">foo</a></h1> 108 | <ul> 109 | <li><a href="#toc_1">foo</a></li> 110 | </ul> 111 | ```````````````````````````````` 112 | 113 | ## HTML in Heading 114 | 115 | ```````````````````````````````` example 116 | # <em>H1</em> 117 | . 118 | <h1 id="toc_1"><em>H1</em></h1> 119 | <ul> 120 | <li><a href="#toc_1">H1</a></li> 121 | </ul> 122 | ```````````````````````````````` 123 | -------------------------------------------------------------------------------- /tests/fixtures/include/hello.md: -------------------------------------------------------------------------------- 1 | > hello 2 | -------------------------------------------------------------------------------- /tests/fixtures/include/hello.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /tests/fixtures/include/text.html: -------------------------------------------------------------------------------- 1 | <div>include html</div> 2 | -------------------------------------------------------------------------------- /tests/fixtures/include/text.md: -------------------------------------------------------------------------------- 1 | # Include 2 | 3 | .. include:: ./text.md 4 | 5 | .. include:: ./not-exist.md 6 | 7 | .. include:: ./text.html 8 | :encoding: utf-8 9 | 10 | .. include:: ./hello.md 11 | 12 | .. include:: ../rst_toc.txt 13 | -------------------------------------------------------------------------------- /tests/fixtures/insert.txt: -------------------------------------------------------------------------------- 1 | # Insert 2 | 3 | ```````````````````````````````` example 4 | ^^insert me 5 | . 6 | <p>^^insert me</p> 7 | ```````````````````````````````` 8 | 9 | 10 | ## normal insert 11 | 12 | ```````````````````````````````` example 13 | ^^insert me^^ 14 | . 15 | <p><ins>insert me</ins></p> 16 | ```````````````````````````````` 17 | 18 | ```````````````````````````````` example 19 | foo ^^insert me^^ 20 | . 21 | <p>foo <ins>insert me</ins></p> 22 | ```````````````````````````````` 23 | 24 | ```````````````````````````````` example 25 | ^^insert me^^foo 26 | . 27 | <p><ins>insert me</ins>foo</p> 28 | ```````````````````````````````` 29 | 30 | ```````````````````````````````` example 31 | ^^insert me^^^ 32 | . 33 | <p>^^insert me^^^</p> 34 | ```````````````````````````````` 35 | 36 | ## escape insert 37 | 38 | ```````````````````````````````` example 39 | ^^insert me\^^^ 40 | . 41 | <p><ins>insert me^</ins></p> 42 | ```````````````````````````````` 43 | 44 | ```````````````````````````````` example 45 | ^^insert \^\^ me^^ 46 | . 47 | <p><ins>insert ^^ me</ins></p> 48 | ```````````````````````````````` 49 | 50 | ## insert link 51 | 52 | ```````````````````````````````` example 53 | ^^insert [me^^](/) 54 | . 55 | <p><ins>insert [me</ins>](/)</p> 56 | ```````````````````````````````` 57 | 58 | ```````````````````````````````` example 59 | ^^[hello](/)^^ 60 | . 61 | <p><ins><a href="/">hello</a></ins></p> 62 | ```````````````````````````````` 63 | -------------------------------------------------------------------------------- /tests/fixtures/mark.txt: -------------------------------------------------------------------------------- 1 | # Mark 2 | 3 | ```````````````````````````````` example 4 | ==mark me 5 | . 6 | <p>==mark me</p> 7 | ```````````````````````````````` 8 | 9 | ## normal mark 10 | 11 | ```````````````````````````````` example 12 | ==mark me== 13 | . 14 | <p><mark>mark me</mark></p> 15 | ```````````````````````````````` 16 | 17 | ```````````````````````````````` example 18 | foo ==mark me== 19 | . 20 | <p>foo <mark>mark me</mark></p> 21 | ```````````````````````````````` 22 | 23 | ```````````````````````````````` example 24 | ==mark me==foo 25 | . 26 | <p><mark>mark me</mark>foo</p> 27 | ```````````````````````````````` 28 | 29 | ```````````````````````````````` example 30 | ==mark me=== 31 | . 32 | <p>==mark me===</p> 33 | ```````````````````````````````` 34 | 35 | ## escape mark 36 | 37 | ```````````````````````````````` example 38 | ==mark me\=== 39 | . 40 | <p><mark>mark me=</mark></p> 41 | ```````````````````````````````` 42 | 43 | ```````````````````````````````` example 44 | ==mark \=\= me== 45 | . 46 | <p><mark>mark == me</mark></p> 47 | ```````````````````````````````` 48 | 49 | ## mark link 50 | 51 | ```````````````````````````````` example 52 | ==mark [me==](/) 53 | . 54 | <p><mark>mark [me</mark>](/)</p> 55 | ```````````````````````````````` 56 | 57 | ```````````````````````````````` example 58 | ==[hello](/)== 59 | . 60 | <p><mark><a href="/">hello</a></mark></p> 61 | ```````````````````````````````` 62 | -------------------------------------------------------------------------------- /tests/fixtures/math.txt: -------------------------------------------------------------------------------- 1 | # Math 2 | 3 | ## Block Math 4 | 5 | ```````````````````````````````` example 6 | $$ 7 | . 8 | <p>$$</p> 9 | ```````````````````````````````` 10 | 11 | 12 | ```````````````````````````````` example 13 | $$ 14 | \operatorname{ker} f=\{g\in G:f(g)=e_{H}\}{\mbox{.}} 15 | $$ 16 | . 17 | <div class="math">$$ 18 | \operatorname{ker} f=\{g\in G:f(g)=e_{H}\}{\mbox{.}} 19 | $$</div> 20 | ```````````````````````````````` 21 | 22 | ```````````````````````````````` example 23 | $$ 24 | foo 25 | $$ 26 | bar 27 | . 28 | <div class="math">$$ 29 | foo 30 | $$</div> 31 | <p>bar</p> 32 | ```````````````````````````````` 33 | 34 | ```````````````````````````````` example 35 | $$ 36 | foo 37 | bar 38 | $$ 39 | . 40 | <div class="math">$$ 41 | foo 42 | bar 43 | $$</div> 44 | ```````````````````````````````` 45 | 46 | 47 | ## Inline Math 48 | 49 | ```````````````````````````````` example 50 | The homomorphism $f$ is injective if and only if its kernel is only the 51 | singleton set $e_G$, because otherwise $\exists a,b\in G$ 52 | with $a\neq b$ such that $f(a)=f(b)$. 53 | . 54 | <p>The homomorphism <span class="math">\(f\)</span> is injective if and only if its kernel is only the 55 | singleton set <span class="math">\(e_G\)</span>, because otherwise <span class="math">\(\exists a,b\in G\)</span> 56 | with <span class="math">\(a\neq b\)</span> such that <span class="math">\(f(a)=f(b)\)</span>.</p> 57 | ```````````````````````````````` 58 | -------------------------------------------------------------------------------- /tests/fixtures/renderer_markdown.txt: -------------------------------------------------------------------------------- 1 | # MarkdownRenderer 2 | 3 | 4 | ## inline 5 | 6 | ```````````````````````````````` example 7 | this is _em_, **strong**, and `code` 8 | . 9 | this is *em*, **strong**, and `code` 10 | ```````````````````````````````` 11 | 12 | ## links 13 | 14 | ```````````````````````````````` example 15 | [link], [link][], [text][link], [link](/foo), [link](/foo "title") 16 | 17 | [link]: /url 18 | . 19 | [link][link], [link][link], [text][link], [link](/foo), [link](/foo "title") 20 | 21 | [link]: /url 22 | ```````````````````````````````` 23 | 24 | ```````````````````````````````` example 25 | [link], [link](https://foo(bar "title") 26 | 27 | [link]: /url "title" 28 | . 29 | [link][link], [link](<https://foo(bar> "title") 30 | 31 | [link]: /url "title" 32 | ```````````````````````````````` 33 | 34 | ```````````````````````````````` example 35 | [https://lepture.com](https://lepture.com) 36 | 37 | <https://lepture.com> 38 | 39 | <demo@example.com> [demo@example.com](mailto:demo@example.com) 40 | . 41 | <https://lepture.com> 42 | 43 | <https://lepture.com> 44 | 45 | <demo@example.com> <demo@example.com> 46 | ```````````````````````````````` 47 | 48 | ## images 49 | 50 | ```````````````````````````````` example 51 | [link]: /url 52 | 53 | ![link], ![link][], ![alt][link] 54 | . 55 | ![link][link], ![link][link], ![alt][link] 56 | 57 | [link]: /url 58 | ```````````````````````````````` 59 | 60 | ## linebreak 61 | 62 | ```````````````````````````````` example 63 | foo\ 64 | bar 65 | . 66 | foo 67 | bar 68 | ```````````````````````````````` 69 | 70 | ## html 71 | 72 | ```````````````````````````````` example 73 | inline html <a>text</a> 74 | 75 | and block html 76 | 77 | <div>hi</div> 78 | . 79 | inline html <a>text</a> 80 | 81 | and block html 82 | 83 | <div>hi</div> 84 | ```````````````````````````````` 85 | 86 | ## heading 87 | 88 | ```````````````````````````````` example 89 | # h1 90 | ## h2 91 | ### h3 92 | #### h4 93 | ##### h5 94 | ###### h6 95 | 96 | h1 97 | == 98 | . 99 | # h1 100 | 101 | ## h2 102 | 103 | ### h3 104 | 105 | #### h4 106 | 107 | ##### h5 108 | 109 | ###### h6 110 | 111 | # h1 112 | ```````````````````````````````` 113 | 114 | ## thematic break 115 | 116 | ```````````````````````````````` example 117 | hi 118 | *** 119 | . 120 | hi 121 | 122 | *** 123 | ```````````````````````````````` 124 | 125 | ## code 126 | 127 | ```````````````````````````````` example 128 | start 129 | 130 | def foo(): 131 | pass 132 | 133 | end 134 | . 135 | start 136 | 137 | ``` 138 | def foo(): 139 | pass 140 | ``` 141 | 142 | end 143 | ```````````````````````````````` 144 | 145 | ```````````````````````````````` example 146 | start 147 | ```py 148 | def foo(): 149 | pass 150 | ``` 151 | end 152 | . 153 | start 154 | 155 | ```py 156 | def foo(): 157 | pass 158 | ``` 159 | 160 | end 161 | ```````````````````````````````` 162 | 163 | ```````````````````````````````` example 164 | indent code 165 | 166 | markdown 167 | ``` 168 | fenced 169 | . 170 | indent code 171 | 172 | ~~~ 173 | markdown 174 | ``` 175 | fenced 176 | ~~~ 177 | ```````````````````````````````` 178 | 179 | ```````````````````````````````` example 180 | indent code 181 | 182 | markdown 183 | ~~~ 184 | fenced 185 | . 186 | indent code 187 | 188 | ``` 189 | markdown 190 | ~~~ 191 | fenced 192 | ``` 193 | ```````````````````````````````` 194 | 195 | ```````````````````````````````` example 196 | indent code 197 | 198 | markdown 199 | ``` 200 | fenced 201 | ~~~ 202 | . 203 | indent code 204 | 205 | ```` 206 | markdown 207 | ``` 208 | fenced 209 | ~~~ 210 | ```` 211 | ```````````````````````````````` 212 | 213 | 214 | ## quote 215 | 216 | ```````````````````````````````` example 217 | > quote 218 | . 219 | > quote 220 | ```````````````````````````````` 221 | 222 | ```````````````````````````````` example 223 | hello 224 | > quote 225 | . 226 | hello 227 | 228 | > quote 229 | ```````````````````````````````` 230 | 231 | > quote 232 | 233 | > quote 234 | > 235 | > continuation 236 | . 237 | > quote 238 | 239 | > quote 240 | > 241 | > continuation 242 | 243 | ## list 244 | 245 | ```````````````````````````````` example 246 | - list 247 | + list 248 | . 249 | - list 250 | 251 | + list 252 | ```````````````````````````````` 253 | 254 | ```````````````````````````````` example 255 | - item 1 256 | item 2 257 | . 258 | - item 1 259 | item 2 260 | ```````````````````````````````` 261 | 262 | ```````````````````````````````` example 263 | - item 1 264 | 265 | item 2 266 | - item 3 267 | . 268 | - item 1 269 | 270 | item 2 271 | 272 | - item 3 273 | ```````````````````````````````` 274 | 275 | ```````````````````````````````` example 276 | - item 1 277 | 1. item 2 278 | . 279 | - item 1 280 | 1. item 2 281 | ```````````````````````````````` 282 | 283 | ```````````````````````````````` example 284 | - item 1 285 | 286 | 2. item 2 287 | . 288 | - item 1 289 | 290 | 2. item 2 291 | ```````````````````````````````` 292 | 293 | ```````````````````````````````` example 294 | - list 295 | - 296 | . 297 | - list 298 | - 299 | ```````````````````````````````` 300 | -------------------------------------------------------------------------------- /tests/fixtures/renderer_rst.txt: -------------------------------------------------------------------------------- 1 | # RSTRenderer 2 | 3 | ## inline 4 | 5 | ```````````````````````````````` example 6 | this is _em_, **strong**, and `code` 7 | . 8 | this is *em*, **strong**, and ``code`` 9 | ```````````````````````````````` 10 | 11 | ```````````````````````````````` example 12 | | escape | 13 | . 14 | \| escape \| 15 | ```````````````````````````````` 16 | 17 | ## links 18 | 19 | ```````````````````````````````` example 20 | this is [link][] and [link](/foo) 21 | 22 | [link]: /url 23 | . 24 | this is `link </url>`__ and `link </foo>`__ 25 | ```````````````````````````````` 26 | 27 | ## images 28 | 29 | ```````````````````````````````` example 30 | this is inline ![alt _text_](/foo) 31 | . 32 | this is inline |img-0| 33 | 34 | .. |img-0| image:: /foo 35 | :alt: alt *text* 36 | ```````````````````````````````` 37 | 38 | ```````````````````````````````` example 39 | this is figure 40 | 41 | ![alt _text_](/foo) 42 | . 43 | this is figure 44 | 45 | .. figure:: /foo 46 | 47 | alt *text* 48 | ```````````````````````````````` 49 | 50 | ```````````````````````````````` example 51 | this is figure 52 | 53 | ![alt _text_](/foo "title") 54 | . 55 | this is figure 56 | 57 | .. figure:: /foo 58 | :alt: title 59 | 60 | alt *text* 61 | ```````````````````````````````` 62 | 63 | 64 | ## html 65 | 66 | ```````````````````````````````` example 67 | ignore inline html <a>text</a> 68 | 69 | but show block html 70 | 71 | <div>hi</div> 72 | . 73 | ignore inline html text 74 | 75 | but show block html 76 | 77 | .. raw:: html 78 | 79 | <div>hi</div> 80 | ```````````````````````````````` 81 | 82 | ## line break 83 | 84 | ```````````````````````````````` example 85 | hello 86 | world 87 | . 88 | | hello 89 | | world 90 | ```````````````````````````````` 91 | 92 | ## heading 93 | 94 | ```````````````````````````````` example 95 | # h1 96 | ## h2 97 | ### h3 98 | #### h4 99 | ##### h5 100 | ###### h6 101 | . 102 | h1 103 | == 104 | 105 | h2 106 | -- 107 | 108 | h3 109 | ~~ 110 | 111 | h4 112 | ^^ 113 | 114 | h5 115 | "" 116 | 117 | h6 118 | '' 119 | ```````````````````````````````` 120 | 121 | ## thematic break 122 | 123 | ```````````````````````````````` example 124 | hi 125 | *** 126 | . 127 | hi 128 | 129 | -------------- 130 | ```````````````````````````````` 131 | 132 | ## code 133 | 134 | ```````````````````````````````` example 135 | start 136 | 137 | def foo(): 138 | pass 139 | 140 | end 141 | . 142 | start 143 | 144 | :: 145 | 146 | def foo(): 147 | pass 148 | 149 | end 150 | ```````````````````````````````` 151 | 152 | ```````````````````````````````` example 153 | start 154 | 155 | ```py 156 | def foo(): 157 | pass 158 | ``` 159 | 160 | end 161 | . 162 | start 163 | 164 | .. code:: py 165 | 166 | def foo(): 167 | pass 168 | 169 | end 170 | ```````````````````````````````` 171 | 172 | 173 | ## quote 174 | 175 | ```````````````````````````````` example 176 | > quote 177 | . 178 | quote 179 | ```````````````````````````````` 180 | 181 | ```````````````````````````````` example 182 | hello 183 | 184 | > quote 185 | . 186 | hello 187 | 188 | quote 189 | ```````````````````````````````` 190 | 191 | ```````````````````````````````` example 192 | > hello 193 | 194 | > quote 195 | . 196 | hello 197 | 198 | .. 199 | 200 | quote 201 | ```````````````````````````````` 202 | 203 | ## list 204 | 205 | ```````````````````````````````` example 206 | - list 207 | + list 208 | . 209 | - list 210 | 211 | + list 212 | ```````````````````````````````` 213 | 214 | ```````````````````````````````` example 215 | - item 1 216 | item 2 217 | . 218 | - item 1 item 2 219 | ```````````````````````````````` 220 | 221 | ```````````````````````````````` example 222 | - item 1 223 | 224 | item 2 225 | - item 3 226 | . 227 | - item 1 228 | 229 | item 2 230 | 231 | - item 3 232 | ```````````````````````````````` 233 | 234 | ```````````````````````````````` example 235 | - item 1 236 | 1. item 2 237 | . 238 | - item 1 239 | 1. item 2 240 | ```````````````````````````````` 241 | 242 | ```````````````````````````````` example 243 | - item 1 244 | 245 | 1. item 2 246 | . 247 | - item 1 248 | 249 | 1. item 2 250 | ```````````````````````````````` 251 | -------------------------------------------------------------------------------- /tests/fixtures/rst_admonition.txt: -------------------------------------------------------------------------------- 1 | # Admonition 2 | 3 | ```````````````````````````````` example 4 | .. hello:: Warning 5 | 6 | message 7 | . 8 | <div class="error"><pre>.. hello:: Warning 9 | 10 | message 11 | </pre></div> 12 | ```````````````````````````````` 13 | 14 | ```````````````````````````````` example 15 | .. note:: Warning 16 | 17 | message 18 | . 19 | <section class="admonition note"> 20 | <p class="admonition-title">Warning</p> 21 | <p>message</p> 22 | </section> 23 | ```````````````````````````````` 24 | 25 | Test with code block. 26 | 27 | ```````````````````````````````` example 28 | .. note:: 29 | 30 | print() 31 | . 32 | <section class="admonition note"> 33 | <p class="admonition-title">Note</p> 34 | <pre><code>print()</code></pre> 35 | </section> 36 | ```````````````````````````````` 37 | 38 | Test with no message. 39 | 40 | ```````````````````````````````` example 41 | .. note:: Warning 42 | . 43 | <section class="admonition note"> 44 | <p class="admonition-title">Warning</p> 45 | </section> 46 | ```````````````````````````````` 47 | -------------------------------------------------------------------------------- /tests/fixtures/rst_toc.txt: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | ## No TOC 4 | 5 | ```````````````````````````````` example 6 | none 7 | . 8 | <p>none</p> 9 | ```````````````````````````````` 10 | 11 | ```````````````````````````````` example 12 | # H1 13 | ## H2 14 | . 15 | <h1>H1</h1> 16 | <h2>H2</h2> 17 | ```````````````````````````````` 18 | 19 | 20 | ## Simple TOC 21 | 22 | ```````````````````````````````` example 23 | .. toc:: 24 | 25 | # H1 26 | ## H2 27 | . 28 | <details class="toc" open> 29 | <summary>Table of Contents</summary> 30 | <ul> 31 | <li><a href="#toc_1">H1</a> 32 | <ul> 33 | <li><a href="#toc_2">H2</a></li> 34 | </ul> 35 | </li> 36 | </ul> 37 | </details> 38 | <h1 id="toc_1">H1</h1> 39 | <h2 id="toc_2">H2</h2> 40 | ```````````````````````````````` 41 | 42 | ## Invalid Option 43 | 44 | ```````````````````````````````` example 45 | # H1 46 | ## H2 47 | 48 | .. toc:: 49 | :max-level: s 50 | . 51 | <h1>H1</h1> 52 | <h2>H2</h2> 53 | <div class="error"><pre>"max-level" option MUST be integer</pre></div> 54 | ```````````````````````````````` 55 | 56 | ```````````````````````````````` example 57 | # H1 58 | ## H2 59 | .. toc:: 60 | :max-level: 9 61 | . 62 | <h1>H1</h1> 63 | <h2>H2</h2> 64 | <div class="error"><pre>"max-level" option MUST be <= 3</pre></div> 65 | ```````````````````````````````` 66 | 67 | ```````````````````````````````` example 68 | # H1 69 | ## H2 70 | .. toc:: 71 | :min-level: 2 72 | :max-level: 1 73 | . 74 | <h1>H1</h1> 75 | <h2>H2</h2> 76 | <div class="error"><pre>"min-level" option MUST be less than "max-level" option</pre></div> 77 | ```````````````````````````````` 78 | 79 | 80 | ## Complex 81 | 82 | ```````````````````````````````` example 83 | # H1 84 | ## H2 85 | ### H3 86 | #### H4 87 | # H1 B 88 | # H1 `C` 89 | 90 | .. toc:: Contents 91 | :max-level: 3 92 | . 93 | <h1 id="toc_1">H1</h1> 94 | <h2 id="toc_2">H2</h2> 95 | <h3 id="toc_3">H3</h3> 96 | <h4 id="toc_4">H4</h4> 97 | <h1 id="toc_5">H1 B</h1> 98 | <h1 id="toc_6">H1 <code>C</code></h1> 99 | <details class="toc" open> 100 | <summary>Contents</summary> 101 | <ul> 102 | <li><a href="#toc_1">H1</a> 103 | <ul> 104 | <li><a href="#toc_2">H2</a> 105 | <ul> 106 | <li><a href="#toc_3">H3</a></li> 107 | </ul> 108 | </li> 109 | </ul> 110 | </li> 111 | <li><a href="#toc_5">H1 B</a></li> 112 | <li><a href="#toc_6">H1 C</a></li> 113 | </ul> 114 | </details> 115 | ```````````````````````````````` 116 | 117 | ## Insane 118 | 119 | ```````````````````````````````` example 120 | # H1 121 | ### H3 122 | ## H2 123 | #### H4 124 | ### H3 B 125 | # H1 B 126 | .. toc:: 127 | . 128 | <h1 id="toc_1">H1</h1> 129 | <h3 id="toc_2">H3</h3> 130 | <h2 id="toc_3">H2</h2> 131 | <h4 id="toc_4">H4</h4> 132 | <h3 id="toc_5">H3 B</h3> 133 | <h1 id="toc_6">H1 B</h1> 134 | <details class="toc" open> 135 | <summary>Table of Contents</summary> 136 | <ul> 137 | <li><a href="#toc_1">H1</a> 138 | <ul> 139 | <li><a href="#toc_2">H3</a></li> 140 | <li><a href="#toc_3">H2</a> 141 | <ul> 142 | <li><a href="#toc_5">H3 B</a></li> 143 | </ul> 144 | </li> 145 | </ul> 146 | </li> 147 | <li><a href="#toc_6">H1 B</a></li> 148 | </ul> 149 | </details> 150 | ```````````````````````````````` 151 | 152 | ```````````````````````````````` example 153 | ### H3 154 | ## H2 155 | # H1 156 | .. toc:: 157 | . 158 | <h3 id="toc_1">H3</h3> 159 | <h2 id="toc_2">H2</h2> 160 | <h1 id="toc_3">H1</h1> 161 | <details class="toc" open> 162 | <summary>Table of Contents</summary> 163 | <ul> 164 | <li><a href="#toc_1">H3</a></li> 165 | <li><a href="#toc_2">H2</a></li> 166 | <li><a href="#toc_3">H1</a></li> 167 | </ul> 168 | </details> 169 | ```````````````````````````````` 170 | 171 | ## Link in Heading 172 | 173 | 174 | ```````````````````````````````` example 175 | # [foo](/bar) 176 | .. toc:: 177 | . 178 | <h1 id="toc_1"><a href="/bar">foo</a></h1> 179 | <details class="toc" open> 180 | <summary>Table of Contents</summary> 181 | <ul> 182 | <li><a href="#toc_1">foo</a></li> 183 | </ul> 184 | </details> 185 | ```````````````````````````````` 186 | 187 | ## HTML in Heading 188 | 189 | ```````````````````````````````` example 190 | # <em>H1</em> 191 | .. toc:: 192 | . 193 | <h1 id="toc_1"><em>H1</em></h1> 194 | <details class="toc" open> 195 | <summary>Table of Contents</summary> 196 | <ul> 197 | <li><a href="#toc_1">H1</a></li> 198 | </ul> 199 | </details> 200 | ```````````````````````````````` 201 | 202 | ## Collapse TOC 203 | 204 | ```````````````````````````````` example 205 | .. toc:: 206 | :collapse: 207 | 208 | # H1 209 | ## H2 210 | . 211 | <details class="toc"> 212 | <summary>Table of Contents</summary> 213 | <ul> 214 | <li><a href="#toc_1">H1</a> 215 | <ul> 216 | <li><a href="#toc_2">H2</a></li> 217 | </ul> 218 | </li> 219 | </ul> 220 | </details> 221 | <h1 id="toc_1">H1</h1> 222 | <h2 id="toc_2">H2</h2> 223 | ```````````````````````````````` 224 | -------------------------------------------------------------------------------- /tests/fixtures/ruby.txt: -------------------------------------------------------------------------------- 1 | # Ruby 2 | 3 | 4 | ## ruby without link 5 | 6 | ```````````````````````````````` example 7 | [漢字(ㄏㄢˋㄗˋ)] 8 | . 9 | <p><ruby>漢字<rt>ㄏㄢˋㄗˋ</rt></ruby></p> 10 | ```````````````````````````````` 11 | 12 | ```````````````````````````````` example 13 | [漢(ㄏㄢˋ)字(ㄗˋ)] 14 | . 15 | <p><ruby>漢<rt>ㄏㄢˋ</rt></ruby><ruby>字<rt>ㄗˋ</rt></ruby></p> 16 | ```````````````````````````````` 17 | 18 | ```````````````````````````````` example 19 | [漢字(ㄏㄢˋㄗˋ)][] 20 | . 21 | <p><ruby>漢字<rt>ㄏㄢˋㄗˋ</rt></ruby>[]</p> 22 | ```````````````````````````````` 23 | 24 | ```````````````````````````````` example 25 | [漢字(ㄏㄢˋㄗˋ)][link] 26 | . 27 | <p><ruby>漢字<rt>ㄏㄢˋㄗˋ</rt></ruby>[link]</p> 28 | ```````````````````````````````` 29 | 30 | ## ruby with link 31 | 32 | ```````````````````````````````` example 33 | [link]: /url 34 | 35 | [漢字(ㄏㄢˋㄗˋ)][link] 36 | . 37 | <p><a href="/url"><ruby>漢字<rt>ㄏㄢˋㄗˋ</rt></ruby></a></p> 38 | ```````````````````````````````` 39 | 40 | ```````````````````````````````` example 41 | [漢字(ㄏㄢˋㄗˋ)]() 42 | . 43 | <p><a href=""><ruby>漢字<rt>ㄏㄢˋㄗˋ</rt></ruby></a></p> 44 | ```````````````````````````````` 45 | 46 | ```````````````````````````````` example 47 | [漢字(ㄏㄢˋㄗˋ)](/url) 48 | . 49 | <p><a href="/url"><ruby>漢字<rt>ㄏㄢˋㄗˋ</rt></ruby></a></p> 50 | ```````````````````````````````` 51 | 52 | ## ruby splits 53 | 54 | ```````````````````````````````` example 55 | [漢(かん)][字(じ)] 56 | . 57 | <p><ruby>漢<rt>かん</rt></ruby><ruby>字<rt>じ</rt></ruby></p> 58 | ```````````````````````````````` 59 | 60 | ```````````````````````````````` example 61 | [[漢(かん)][字(じ)]](https://jisho.org/search/漢字) 62 | . 63 | <p><a href="https://jisho.org/search/%E6%BC%A2%E5%AD%97"><ruby>漢<rt>かん</rt></ruby><ruby>字<rt>じ</rt></ruby></a></p> 64 | ```````````````````````````````` 65 | 66 | ```````````````````````````````` example 67 | [汉字(hàn zì)] 68 | . 69 | <p><ruby>汉字<rt>hàn zì</rt></ruby></p> 70 | ```````````````````````````````` 71 | -------------------------------------------------------------------------------- /tests/fixtures/spoiler.txt: -------------------------------------------------------------------------------- 1 | # Spoiler 2 | 3 | ## Block spoiler 4 | 5 | ```````````````````````````````` example 6 | >! hi 7 | . 8 | <div class="spoiler"> 9 | <p>hi</p> 10 | </div> 11 | ```````````````````````````````` 12 | 13 | 14 | ```````````````````````````````` example 15 | >! not 16 | > spoiler 17 | . 18 | <blockquote> 19 | <p>! not 20 | spoiler</p> 21 | </blockquote> 22 | ```````````````````````````````` 23 | 24 | ```````````````````````````````` example 25 | >! multiple 26 | >! lines 27 | . 28 | <div class="spoiler"> 29 | <p>multiple 30 | lines</p> 31 | </div> 32 | ```````````````````````````````` 33 | 34 | ```````````````````````````````` example 35 | >! foo 36 | >! 37 | >! bar 38 | . 39 | <div class="spoiler"> 40 | <p>foo</p> 41 | <p>bar</p> 42 | </div> 43 | ```````````````````````````````` 44 | 45 | ```````````````````````````````` example 46 | >! foo 47 | > 48 | >! bar 49 | . 50 | <blockquote> 51 | <p>! foo</p> 52 | <p>! bar</p> 53 | </blockquote> 54 | ```````````````````````````````` 55 | 56 | ## Inline spoiler 57 | 58 | ```````````````````````````````` example 59 | hi >! hide !< 60 | . 61 | <p>hi <span class="spoiler">hide</span></p> 62 | ```````````````````````````````` 63 | 64 | ```````````````````````````````` example 65 | hi >!hide !< 66 | . 67 | <p>hi <span class="spoiler">hide</span></p> 68 | ```````````````````````````````` 69 | 70 | ```````````````````````````````` example 71 | hi >!hide!< 72 | . 73 | <p>hi <span class="spoiler">hide</span></p> 74 | ```````````````````````````````` 75 | -------------------------------------------------------------------------------- /tests/fixtures/strikethrough.txt: -------------------------------------------------------------------------------- 1 | # Strikethrough 2 | 3 | 4 | ## strikethrough 5 | 6 | ```````````````````````````````` example 7 | ~~delete me 8 | . 9 | <p>~~delete me</p> 10 | ```````````````````````````````` 11 | 12 | ```````````````````````````````` example 13 | ~~delete me~~ 14 | . 15 | <p><del>delete me</del></p> 16 | ```````````````````````````````` 17 | 18 | ```````````````````````````````` example 19 | ~~delete me~~~ 20 | . 21 | <p>~~delete me~~~</p> 22 | ```````````````````````````````` 23 | 24 | ```````````````````````````````` example 25 | ~~delete me\~~~ 26 | . 27 | <p><del>delete me~</del></p> 28 | ```````````````````````````````` 29 | 30 | ```````````````````````````````` example 31 | ~~delete \~\~ me~~ 32 | . 33 | <p><del>delete ~~ me</del></p> 34 | ```````````````````````````````` 35 | 36 | ```````````````````````````````` example 37 | ~~delete [me~~](/) 38 | . 39 | <p><del>delete [me</del>](/)</p> 40 | ```````````````````````````````` 41 | 42 | ```````````````````````````````` example 43 | ~~[hello](/)~~ 44 | . 45 | <p><del><a href="/">hello</a></del></p> 46 | ```````````````````````````````` 47 | -------------------------------------------------------------------------------- /tests/fixtures/subscript.txt: -------------------------------------------------------------------------------- 1 | # Subscript 2 | 3 | 4 | ```````````````````````````````` example 5 | CH~3~CH~2~OH 6 | . 7 | <p>CH<sub>3</sub>CH<sub>2</sub>OH</p> 8 | ```````````````````````````````` 9 | 10 | ```````````````````````````````` example 11 | text~a\ subscript~ 12 | . 13 | <p>text<sub>a subscript</sub></p> 14 | ```````````````````````````````` 15 | 16 | ```````````````````````````````` example 17 | text~a subscript~ 18 | . 19 | <p>text~a subscript~</p> 20 | ```````````````````````````````` 21 | 22 | ```````````````````````````````` example 23 | text~a\~subscript~ 24 | . 25 | <p>text<sub>a~subscript</sub></p> 26 | ```````````````````````````````` 27 | -------------------------------------------------------------------------------- /tests/fixtures/superscript.txt: -------------------------------------------------------------------------------- 1 | # Superscript 2 | 3 | 4 | ```````````````````````````````` example 5 | CH^3^CH^2^OH 6 | . 7 | <p>CH<sup>3</sup>CH<sup>2</sup>OH</p> 8 | ```````````````````````````````` 9 | 10 | ```````````````````````````````` example 11 | text^a\ supscript^ 12 | . 13 | <p>text<sup>a supscript</sup></p> 14 | ```````````````````````````````` 15 | 16 | ```````````````````````````````` example 17 | text^a supscript^ 18 | . 19 | <p>text^a supscript^</p> 20 | ```````````````````````````````` 21 | 22 | ```````````````````````````````` example 23 | text^a\^supscript^ 24 | . 25 | <p>text<sup>a^supscript</sup></p> 26 | ```````````````````````````````` 27 | -------------------------------------------------------------------------------- /tests/fixtures/table.txt: -------------------------------------------------------------------------------- 1 | # Table in Markdown 2 | 3 | https://michelf.ca/projects/php-markdown/extra/#table 4 | 5 | 6 | ## nptable 7 | 8 | 9 | ```````````````````````````````` example 10 | First Header | Second Header 11 | ------------- | ------------- 12 | Content Cell | Content Cell 13 | Content Cell | Content Cell 14 | . 15 | <table> 16 | <thead> 17 | <tr> 18 | <th>First Header</th> 19 | <th>Second Header</th> 20 | </tr> 21 | </thead> 22 | <tbody> 23 | <tr> 24 | <td>Content Cell</td> 25 | <td>Content Cell</td> 26 | </tr> 27 | <tr> 28 | <td>Content Cell</td> 29 | <td>Content Cell</td> 30 | </tr> 31 | </tbody> 32 | </table> 33 | ```````````````````````````````` 34 | 35 | 36 | ## table 37 | 38 | ```````````````````````````````` example 39 | | First Header | Second Header | 40 | | ------------- | ------------- | 41 | | Content Cell | | 42 | | Content Cell | Content Cell | 43 | . 44 | <table> 45 | <thead> 46 | <tr> 47 | <th>First Header</th> 48 | <th>Second Header</th> 49 | </tr> 50 | </thead> 51 | <tbody> 52 | <tr> 53 | <td>Content Cell</td> 54 | <td></td> 55 | </tr> 56 | <tr> 57 | <td>Content Cell</td> 58 | <td>Content Cell</td> 59 | </tr> 60 | </tbody> 61 | </table> 62 | ```````````````````````````````` 63 | 64 | 65 | ```````````````````````````````` example 66 | | Command | Description | 67 | | --- | --- | 68 | | git status | List all new or modified files | 69 | | git diff | Show file differences that haven't been staged | 70 | . 71 | <table> 72 | <thead> 73 | <tr> 74 | <th>Command</th> 75 | <th>Description</th> 76 | </tr> 77 | </thead> 78 | <tbody> 79 | <tr> 80 | <td>git status</td> 81 | <td>List all new or modified files</td> 82 | </tr> 83 | <tr> 84 | <td>git diff</td> 85 | <td>Show file differences that haven't been staged</td> 86 | </tr> 87 | </tbody> 88 | </table> 89 | ```````````````````````````````` 90 | 91 | ## align table 92 | 93 | ```````````````````````````````` example 94 | Left | Center | Right 95 | :---- | :--------: | ------: 96 | a | b | c 97 | . 98 | <table> 99 | <thead> 100 | <tr> 101 | <th style="text-align:left">Left</th> 102 | <th style="text-align:center">Center</th> 103 | <th style="text-align:right">Right</th> 104 | </tr> 105 | </thead> 106 | <tbody> 107 | <tr> 108 | <td style="text-align:left">a</td> 109 | <td style="text-align:center">b</td> 110 | <td style="text-align:right">c</td> 111 | </tr> 112 | </tbody> 113 | </table> 114 | ```````````````````````````````` 115 | 116 | ```````````````````````````````` example 117 | | Left | Center | Right | 118 | | :--- | :----: | ----: | 119 | | a | b | c | 120 | . 121 | <table> 122 | <thead> 123 | <tr> 124 | <th style="text-align:left">Left</th> 125 | <th style="text-align:center">Center</th> 126 | <th style="text-align:right">Right</th> 127 | </tr> 128 | </thead> 129 | <tbody> 130 | <tr> 131 | <td style="text-align:left">a</td> 132 | <td style="text-align:center">b</td> 133 | <td style="text-align:right">c</td> 134 | </tr> 135 | </tbody> 136 | </table> 137 | ```````````````````````````````` 138 | 139 | ## Misc Table 140 | 141 | ```````````````````````````````` example 142 | | A | | 143 | | - | | 144 | | a | b | 145 | . 146 | <table> 147 | <thead> 148 | <tr> 149 | <th>A</th> 150 | <th></th> 151 | </tr> 152 | </thead> 153 | <tbody> 154 | <tr> 155 | <td>a</td> 156 | <td>b</td> 157 | </tr> 158 | </tbody> 159 | </table> 160 | ```````````````````````````````` 161 | 162 | ```````````````````````````````` example 163 | A | 164 | - | 165 | a | b 166 | . 167 | <table> 168 | <thead> 169 | <tr> 170 | <th>A</th> 171 | <th></th> 172 | </tr> 173 | </thead> 174 | <tbody> 175 | <tr> 176 | <td>a</td> 177 | <td>b</td> 178 | </tr> 179 | </tbody> 180 | </table> 181 | ```````````````````````````````` 182 | 183 | 184 | ```````````````````````````````` example 185 | | Foo | Bar | 186 | |-----|-----| 187 | | boo | baz | 188 | | faz | | 189 | . 190 | <table> 191 | <thead> 192 | <tr> 193 | <th>Foo</th> 194 | <th>Bar</th> 195 | </tr> 196 | </thead> 197 | <tbody> 198 | <tr> 199 | <td>boo</td> 200 | <td>baz</td> 201 | </tr> 202 | <tr> 203 | <td>faz</td> 204 | <td></td> 205 | </tr> 206 | </tbody> 207 | </table> 208 | ```````````````````````````````` 209 | 210 | ## Mismatch length 211 | 212 | ```````````````````````````````` example 213 | | First Header | 214 | | ------------- | ------------- | 215 | | Content Cell | | 216 | | Content Cell | Content Cell | 217 | . 218 | <p>| First Header | 219 | | ------------- | ------------- | 220 | | Content Cell | | 221 | | Content Cell | Content Cell |</p> 222 | ```````````````````````````````` 223 | 224 | ```````````````````````````````` example 225 | | First Header | Second Header | 226 | | ------------- | ------------- | 227 | | Content Cell | 228 | | Content Cell | Content Cell | 229 | . 230 | <p>| First Header | Second Header | 231 | | ------------- | ------------- | 232 | | Content Cell | 233 | | Content Cell | Content Cell |</p> 234 | ```````````````````````````````` 235 | 236 | ```````````````````````````````` example 237 | A | 238 | - | - | 239 | a | b | c 240 | . 241 | <p>A |</p> 242 | <ul> 243 | <li>| - | 244 | a | b | c</li> 245 | </ul> 246 | ```````````````````````````````` 247 | 248 | ```````````````````````````````` example 249 | A | B | C 250 | - | - | - 251 | a | b 252 | . 253 | <p>A | B | C</p> 254 | <ul> 255 | <li>| - | - 256 | a | b</li> 257 | </ul> 258 | ```````````````````````````````` 259 | -------------------------------------------------------------------------------- /tests/fixtures/task_lists.txt: -------------------------------------------------------------------------------- 1 | # Task Lists 2 | 3 | ```````````````````````````````` example 4 | - [ ] foo 5 | - [x] bar 6 | - [X] baz 7 | . 8 | <ul> 9 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>foo</li> 10 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>bar</li> 11 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>baz</li> 12 | </ul> 13 | ```````````````````````````````` 14 | 15 | ```````````````````````````````` example 16 | - foo 17 | - [ ] bar 18 | . 19 | <ul> 20 | <li>foo</li> 21 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>bar</li> 22 | </ul> 23 | ```````````````````````````````` 24 | 25 | ```````````````````````````````` example 26 | - foo 27 | 28 | - [ ] bar 29 | - [x] baz 30 | 31 | text 32 | . 33 | <ul> 34 | <li><p>foo</p> 35 | </li> 36 | <li class="task-list-item"><p><input class="task-list-item-checkbox" type="checkbox" disabled/>bar</p> 37 | </li> 38 | <li class="task-list-item"><p><input class="task-list-item-checkbox" type="checkbox" disabled checked/>baz</p> 39 | <p>text</p> 40 | </li> 41 | </ul> 42 | ```````````````````````````````` 43 | 44 | ```````````````````````````````` example 45 | 1. [ ] foo 46 | 2. [x] bar 47 | 3. [X] baz 48 | . 49 | <ol> 50 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>foo</li> 51 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>bar</li> 52 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>baz</li> 53 | </ol> 54 | ```````````````````````````````` 55 | 56 | ```````````````````````````````` example 57 | - [ ] Task list item 58 | over two lines 59 | . 60 | <ul> 61 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>Task list item 62 | over two lines</li> 63 | </ul> 64 | ```````````````````````````````` 65 | 66 | ```````````````````````````````` example 67 | - [ ] foo 68 | - [x] bar 69 | - [ ] baz 70 | . 71 | <ul> 72 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>foo<ul> 73 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>bar</li> 74 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>baz</li> 75 | </ul> 76 | </li> 77 | </ul> 78 | ```````````````````````````````` 79 | 80 | ```````````````````````````````` example 81 | 1. [ ] foo 82 | 1. [ ] bar 83 | 2. [ ] baz 84 | . 85 | <ol> 86 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>foo<ol> 87 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>bar</li> 88 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>baz</li> 89 | </ol> 90 | </li> 91 | </ol> 92 | ```````````````````````````````` 93 | 94 | ```````````````````````````````` example 95 | > - [ ] foo 96 | > - [x] bar 97 | > - [ ] baz 98 | . 99 | <blockquote> 100 | <ul> 101 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>foo<ul> 102 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled checked/>bar</li> 103 | <li class="task-list-item"><input class="task-list-item-checkbox" type="checkbox" disabled/>baz</li> 104 | </ul> 105 | </li> 106 | </ul> 107 | </blockquote> 108 | ```````````````````````````````` -------------------------------------------------------------------------------- /tests/fixtures/url.txt: -------------------------------------------------------------------------------- 1 | # url plugin 2 | 3 | 4 | ```````````````````````````````` example 5 | visit https://lepture.com/ 6 | . 7 | <p>visit <a href="https://lepture.com/">https://lepture.com/</a></p> 8 | ```````````````````````````````` 9 | 10 | 11 | ```````````````````````````````` example 12 | visit [https://lepture.com/](/foo) 13 | . 14 | <p>visit <a href="/foo">https://lepture.com/</a></p> 15 | ```````````````````````````````` 16 | 17 | 18 | ```````````````````````````````` example 19 | visit <a href="/foo">https://lepture.com/</a> 20 | . 21 | <p>visit <a href="/foo">https://lepture.com/</a></p> 22 | ```````````````````````````````` 23 | -------------------------------------------------------------------------------- /tests/test_commonmark.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | from tests import BaseTestCase, normalize_html 3 | 4 | 5 | DIFF_CASES = { 6 | "setext_headings_093", 7 | "html_blocks_191", # mistune keeps \n 8 | "images_573", # image can not be in image 9 | "links_495", 10 | "links_517", # aggressive link group 11 | "links_518", 12 | "links_519", 13 | "links_531", 14 | "links_532", 15 | } 16 | 17 | IGNORE_CASES = { 18 | # we don't support link title in (title) 19 | "links_496", 20 | "links_504", 21 | # we don't support flanking delimiter run 22 | "emphasis_and_strong_emphasis_352", 23 | "emphasis_and_strong_emphasis_367", 24 | "emphasis_and_strong_emphasis_368", 25 | "emphasis_and_strong_emphasis_372", 26 | "emphasis_and_strong_emphasis_379", 27 | "emphasis_and_strong_emphasis_388", 28 | "emphasis_and_strong_emphasis_391", 29 | "emphasis_and_strong_emphasis_406", 30 | "emphasis_and_strong_emphasis_407", 31 | "emphasis_and_strong_emphasis_408", 32 | "emphasis_and_strong_emphasis_412", 33 | "emphasis_and_strong_emphasis_413", 34 | "emphasis_and_strong_emphasis_414", 35 | "emphasis_and_strong_emphasis_416", 36 | "emphasis_and_strong_emphasis_417", 37 | "emphasis_and_strong_emphasis_418", 38 | "emphasis_and_strong_emphasis_424", 39 | "emphasis_and_strong_emphasis_425", 40 | "emphasis_and_strong_emphasis_426", 41 | "emphasis_and_strong_emphasis_429", 42 | "emphasis_and_strong_emphasis_430", 43 | "emphasis_and_strong_emphasis_431", 44 | "emphasis_and_strong_emphasis_460", 45 | "emphasis_and_strong_emphasis_467", 46 | "emphasis_and_strong_emphasis_470", 47 | "emphasis_and_strong_emphasis_471", 48 | "emphasis_and_strong_emphasis_477", 49 | "emphasis_and_strong_emphasis_478", 50 | } 51 | 52 | for i in range(441, 447): 53 | IGNORE_CASES.add("emphasis_and_strong_emphasis_" + str(i)) 54 | for i in range(453, 459): 55 | IGNORE_CASES.add("emphasis_and_strong_emphasis_" + str(i)) 56 | for i in range(462, 466): 57 | IGNORE_CASES.add("emphasis_and_strong_emphasis_" + str(i)) 58 | 59 | 60 | class TestCommonMark(BaseTestCase): 61 | @classmethod 62 | def ignore_case(cls, n): 63 | return n in IGNORE_CASES or n in DIFF_CASES 64 | 65 | def assert_case(self, n, text, html): 66 | result = mistune.html(text) 67 | self.assertEqual(normalize_html(result), normalize_html(html)) 68 | 69 | 70 | TestCommonMark.load_fixtures("commonmark.json") 71 | -------------------------------------------------------------------------------- /tests/test_directives.py: -------------------------------------------------------------------------------- 1 | import os 2 | from mistune import create_markdown 3 | from mistune.directives import ( 4 | RSTDirective, 5 | FencedDirective, 6 | Admonition, 7 | TableOfContents, 8 | Include, 9 | Image, 10 | Figure, 11 | ) 12 | from tests import BaseTestCase 13 | from tests.fixtures import ROOT 14 | 15 | 16 | def load_directive_test(filename, directive, cls): 17 | class TestDirective(BaseTestCase): 18 | @staticmethod 19 | def parse(text): 20 | md = create_markdown( 21 | escape=False, 22 | plugins=[cls([directive])], 23 | ) 24 | html = md(text) 25 | return html 26 | 27 | TestDirective.load_fixtures(filename + ".txt") 28 | globals()["TestDirective_" + filename] = TestDirective 29 | 30 | 31 | load_directive_test("rst_admonition", Admonition(), RSTDirective) 32 | load_directive_test("rst_toc", TableOfContents(), RSTDirective) 33 | load_directive_test("fenced_admonition", Admonition(), FencedDirective) 34 | load_directive_test("fenced_toc", TableOfContents(), FencedDirective) 35 | load_directive_test("fenced_image", Image(), FencedDirective) 36 | load_directive_test("fenced_figure", Figure(), FencedDirective) 37 | 38 | 39 | class CustomizeTableOfContents(TableOfContents): 40 | def generate_heading_id(self, token, i): 41 | return "t-" + str(i + 1) 42 | 43 | 44 | class TestCustomizeToc(BaseTestCase): 45 | def test_rst_toc(self): 46 | md = create_markdown( 47 | escape=False, 48 | plugins=[ 49 | RSTDirective([CustomizeTableOfContents()]), 50 | ], 51 | ) 52 | html = md("# h1\n\n.. toc::\n") 53 | self.assertIn('<h1 id="t-1">h1</h1>', html) 54 | self.assertIn('<a href="#t-1">h1</a>', html) 55 | 56 | def test_fenced_toc(self): 57 | md = create_markdown( 58 | escape=False, 59 | plugins=[ 60 | FencedDirective([CustomizeTableOfContents()]), 61 | ], 62 | ) 63 | html = md("# h1\n\n```{toc}\n```\n") 64 | self.assertIn('<h1 id="t-1">h1</h1>', html) 65 | self.assertIn('<a href="#t-1">h1</a>', html) 66 | 67 | def test_colon_fenced_toc(self): 68 | md = create_markdown( 69 | escape=False, 70 | plugins=[ 71 | FencedDirective([CustomizeTableOfContents()], ":"), 72 | ], 73 | ) 74 | html = md("# h1\n\n:::{toc}\n:::\n") 75 | self.assertIn('<h1 id="t-1">h1</h1>', html) 76 | self.assertIn('<a href="#t-1">h1</a>', html) 77 | 78 | 79 | class TestDirectiveInclude(BaseTestCase): 80 | md = create_markdown(escape=False, plugins=[RSTDirective([Include()])]) # type: ignore[list-item] 81 | 82 | def test_html_include(self): 83 | html = self.md.read(os.path.join(ROOT, "include/text.md"))[0] 84 | self.assertIn("Could not include self", html) 85 | self.assertIn("Could not find file", html) 86 | self.assertIn("<div>include html</div>", html) 87 | self.assertIn("<blockquote>", html) 88 | self.assertIn("# Table of Contents", html) 89 | 90 | def test_include_missing_source(self): 91 | s = ".. include:: foo.txt" 92 | html = self.md(s) 93 | self.assertIn("Missing source file", html) 94 | -------------------------------------------------------------------------------- /tests/test_hooks.py: -------------------------------------------------------------------------------- 1 | import os 2 | from mistune import create_markdown 3 | from mistune.toc import add_toc_hook, render_toc_ul 4 | from tests import BaseTestCase 5 | 6 | 7 | class TestTocHook(BaseTestCase): 8 | @staticmethod 9 | def parse(text): 10 | md = create_markdown(escape=False) 11 | add_toc_hook(md) 12 | html, state = md.parse(text) 13 | result = html + render_toc_ul(state.env["toc_items"]) 14 | return result 15 | 16 | def test_customize_heading_id_func(self): 17 | def heading_id(token, i): 18 | return "t-" + str(i + 1) 19 | 20 | md = create_markdown(escape=False) 21 | add_toc_hook(md, heading_id=heading_id) 22 | html = md("# h1") 23 | self.assertEqual(html, '<h1 id="t-1">h1</h1>\n') 24 | 25 | def test_render_empty_toc(self): 26 | self.assertEqual(render_toc_ul([]), "") 27 | self.assertEqual(render_toc_ul(filter(lambda _: False, [])), "") 28 | 29 | 30 | TestTocHook.load_fixtures("hook_toc.txt") 31 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | from unittest import TestCase 3 | 4 | 5 | class TestMiscCases(TestCase): 6 | def test_none(self): 7 | self.assertEqual(mistune.html(None), "") 8 | 9 | def test_before_parse_hooks(self): 10 | def _add_name(md, state): 11 | state.env["name"] = "test" 12 | 13 | md = mistune.create_markdown() 14 | md.before_parse_hooks.append(_add_name) 15 | state = md.block.state_cls() 16 | md.parse("", state) 17 | self.assertEqual(state.env["name"], "test") 18 | 19 | def test_hard_wrap(self): 20 | md = mistune.create_markdown(escape=False, hard_wrap=True) 21 | result = md("foo\nbar") 22 | expected = "<p>foo<br />\nbar</p>" 23 | self.assertEqual(result.strip(), expected) 24 | 25 | md = mistune.create_markdown(escape=False, hard_wrap=True, plugins=["speedup"]) 26 | result = md("foo\nbar") 27 | self.assertEqual(result.strip(), expected) 28 | 29 | def test_escape_html(self): 30 | md = mistune.create_markdown(escape=True) 31 | result = md("<div>1</div>") 32 | expected = "<p><div>1</div></p>" 33 | self.assertEqual(result.strip(), expected) 34 | 35 | result = md("<em>1</em>") 36 | expected = "<p><em>1</em></p>" 37 | self.assertEqual(result.strip(), expected) 38 | 39 | def test_harmful_links(self): 40 | result = mistune.html("[h](javAscript:alert)") 41 | expected = '<p><a href="#harmful-link">h</a></p>' 42 | self.assertEqual(result.strip(), expected) 43 | 44 | def test_ref_link(self): 45 | result = mistune.html("[link][h]\n\n[h]: /foo") 46 | expected = '<p><a href="/foo">link</a></p>' 47 | self.assertEqual(result.strip(), expected) 48 | 49 | def test_allow_harmful_protocols(self): 50 | renderer = mistune.HTMLRenderer(allow_harmful_protocols=True) 51 | md = mistune.Markdown(renderer) 52 | result = md("[h](javascript:alert)") 53 | expected = '<p><a href="javascript:alert">h</a></p>' 54 | self.assertEqual(result.strip(), expected) 55 | 56 | def test_allow_data_protocols(self): 57 | renderer = mistune.HTMLRenderer(allow_harmful_protocols=["data:"]) 58 | md = mistune.Markdown(renderer) 59 | result = md("[h](data:alert)") 60 | expected = '<p><a href="data:alert">h</a></p>' 61 | self.assertEqual(result.strip(), expected) 62 | 63 | def test_use_plugin(self): 64 | from mistune.plugins.url import url 65 | 66 | md = mistune.Markdown(mistune.HTMLRenderer()) 67 | md.use(url) 68 | 69 | def test_markdown_func(self): 70 | result = mistune.markdown("**b**") 71 | expected = "<p><strong>b</strong></p>\n" 72 | self.assertEqual(result, expected) 73 | 74 | # trigger to use cached parser 75 | result = mistune.markdown("**b**") 76 | self.assertEqual(result, expected) 77 | 78 | def test_ast_output(self): 79 | md = mistune.create_markdown(escape=False, renderer=None) 80 | text = '# h1\n\nfoo **bar**\n\n`&<>"`' 81 | result = md(text) 82 | expected = [ 83 | { 84 | "type": "heading", 85 | "children": [{"type": "text", "raw": "h1"}], 86 | "attrs": {"level": 1}, 87 | "style": "atx", 88 | }, 89 | {"type": "blank_line"}, 90 | { 91 | "type": "paragraph", 92 | "children": [ 93 | {"type": "text", "raw": "foo "}, 94 | {"type": "strong", "children": [{"type": "text", "raw": "bar"}]}, 95 | ], 96 | }, 97 | {"type": "blank_line"}, 98 | { 99 | "type": "paragraph", 100 | "children": [ 101 | {"type": "codespan", "raw": '&<>"'}, 102 | ], 103 | }, 104 | ] 105 | self.assertEqual(result, expected) 106 | 107 | def test_ast_url(self): 108 | md = mistune.create_markdown(escape=False, renderer=None) 109 | label = 'hi &<>"' 110 | url = "https://example.com/foo?a=1&b=2" 111 | text = "[{}]({})".format(label, url) 112 | result = md(text) 113 | expected = [ 114 | { 115 | "type": "paragraph", 116 | "children": [ 117 | { 118 | "type": "link", 119 | "children": [{"type": "text", "raw": label}], 120 | "attrs": {"url": url}, 121 | }, 122 | ], 123 | }, 124 | ] 125 | self.assertEqual(result, expected) 126 | 127 | def test_emsp(self): 128 | md = mistune.create_markdown(escape=False, hard_wrap=True) 129 | result = md("\u2003\u2003foo\nbar\n\n\u2003\u2003foobar") 130 | expected = "<p>\u2003\u2003foo<br />\nbar</p>\n<p>\u2003\u2003foobar</p>" 131 | self.assertEqual(result.strip(), expected) 132 | 133 | def test_html_tag_text_following_list(self): 134 | md = mistune.create_markdown(escape=False, hard_wrap=True) 135 | result = md("foo\n- bar\n\ntable") 136 | expected = "<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n<p>table</p>" 137 | self.assertEqual(result.strip(), expected) 138 | -------------------------------------------------------------------------------- /tests/test_plugins.py: -------------------------------------------------------------------------------- 1 | from mistune import create_markdown 2 | from mistune.plugins.table import table_in_list, table_in_quote 3 | from mistune.plugins.math import math_in_list, math_in_quote 4 | from tests import BaseTestCase 5 | 6 | 7 | def load_plugin(plugin_name): 8 | md1 = create_markdown(escape=False, plugins=[plugin_name]) 9 | md2 = create_markdown(escape=False, plugins=[plugin_name, "speedup"]) 10 | 11 | class TestPlugin1(BaseTestCase): 12 | parse = md1 13 | 14 | class TestPlugin2(BaseTestCase): 15 | parse = md2 16 | 17 | TestPlugin1.load_fixtures(plugin_name + ".txt") 18 | TestPlugin2.load_fixtures(plugin_name + ".txt") 19 | globals()["TestPlugin1_" + plugin_name] = TestPlugin1 20 | globals()["TestPlugin2_" + plugin_name] = TestPlugin2 21 | 22 | 23 | load_plugin("url") 24 | load_plugin("strikethrough") 25 | load_plugin("mark") 26 | load_plugin("insert") 27 | load_plugin("superscript") 28 | load_plugin("subscript") 29 | load_plugin("task_lists") 30 | load_plugin("table") 31 | load_plugin("def_list") 32 | load_plugin("footnotes") 33 | load_plugin("abbr") 34 | load_plugin("math") 35 | load_plugin("ruby") 36 | load_plugin("spoiler") 37 | 38 | 39 | class TestExtraPlugins(BaseTestCase): 40 | def test_table_in_list(self): 41 | text = """- Cell | Cell\n ---- | ----\n 1 | 2\n""" 42 | md1 = create_markdown(escape=False) 43 | md2 = create_markdown(escape=False, plugins=["table", table_in_list]) 44 | self.assertNotIn("<table>", md1(text)) 45 | self.assertIn("<table>", md2(text)) 46 | 47 | def test_table_in_quote(self): 48 | text = """> Cell | Cell\n> ---- | ----\n> 1 | 2\n""" 49 | md1 = create_markdown(escape=False) 50 | md2 = create_markdown(escape=False, plugins=["table", table_in_quote]) 51 | self.assertNotIn("<table>", md1(text)) 52 | self.assertIn("<table>", md2(text)) 53 | 54 | def test_math_in_list(self): 55 | text = """- $$\n foo\n $$\n""" 56 | md1 = create_markdown(escape=False) 57 | md2 = create_markdown(escape=False, plugins=["math", math_in_list]) 58 | self.assertNotIn('class="math"', md1(text)) 59 | self.assertIn('class="math"', md2(text)) 60 | 61 | def test_math_in_quote(self): 62 | text = """> $$\n> foo\n> $$\n""" 63 | md1 = create_markdown(escape=False) 64 | md2 = create_markdown(escape=False, plugins=["math", math_in_quote]) 65 | self.assertNotIn('class="math"', md1(text)) 66 | self.assertIn('class="math"', md2(text)) 67 | -------------------------------------------------------------------------------- /tests/test_renderers.py: -------------------------------------------------------------------------------- 1 | from mistune import create_markdown 2 | from mistune.renderers.rst import RSTRenderer 3 | from mistune.renderers.markdown import MarkdownRenderer 4 | from tests import BaseTestCase 5 | 6 | 7 | def load_renderer(renderer): 8 | class TestRenderer(BaseTestCase): 9 | parse = create_markdown(renderer=renderer) 10 | 11 | name = renderer.NAME 12 | TestRenderer.load_fixtures("renderer_" + name + ".txt") 13 | globals()["TestRenderer" + name.title()] = TestRenderer 14 | 15 | 16 | load_renderer(RSTRenderer()) 17 | load_renderer(MarkdownRenderer()) 18 | -------------------------------------------------------------------------------- /tests/test_syntax.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | from tests import BaseTestCase, normalize_html 3 | 4 | 5 | class TestSyntax(BaseTestCase): 6 | def assert_case(self, n, text, html): 7 | result = mistune.html(text) 8 | self.assertEqual(normalize_html(result), normalize_html(html)) 9 | 10 | 11 | TestSyntax.load_fixtures("fix-commonmark.txt") 12 | TestSyntax.load_fixtures("diff-commonmark.txt") 13 | --------------------------------------------------------------------------------