Import Upstream version 0.18.1

This commit is contained in:
su-fang 2023-04-20 10:00:02 +08:00
commit aff43e4ee7
201 changed files with 25925 additions and 0 deletions

56
.github/workflows/docutils_setup.py vendored Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Script to convert package setup to myst-docutils."""
import sys
import tomlkit
def modify_toml(content: str) -> str:
"""Modify `pyproject.toml`."""
doc = tomlkit.parse(content)
# change name of package
doc["project"]["name"] = "myst-docutils"
# move dependency on docutils and sphinx to extra
dependencies = []
sphinx_extra = []
for dep in doc["project"]["dependencies"]:
if dep.startswith("docutils") or dep.startswith("sphinx"):
sphinx_extra.append(dep)
else:
dependencies.append(dep)
doc["project"]["dependencies"] = dependencies
doc["project"]["optional-dependencies"]["sphinx"] = sphinx_extra
return tomlkit.dumps(doc)
def modify_readme(content: str) -> str:
"""Modify README.md."""
content = content.replace("myst-parser", "myst-docutils")
content = content.replace(
"# MyST-Parser",
"# MyST-Parser\n\nNote: myst-docutils is identical to myst-parser, "
"but without installation requirements on sphinx",
)
content = content.replace("myst-docutils.readthedocs", "myst-parser.readthedocs")
content = content.replace(
"readthedocs.org/projects/myst-docutils", "readthedocs.org/projects/myst-parser"
)
return content
if __name__ == "__main__":
project_path = sys.argv[1]
readme_path = sys.argv[2]
with open(project_path) as f:
content = f.read()
content = modify_toml(content)
with open(project_path, "w") as f:
f.write(content)
with open(readme_path) as f:
content = f.read()
content = modify_readme(content)
with open(readme_path, "w") as f:
f.write(content)

152
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,152 @@
name: continuous-integration
on:
push:
branches: [master]
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
pull_request:
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: "3.8"
- uses: pre-commit/action@v2.0.0
tests:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
sphinx: [">=5,<6"]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
python-version: "3.8"
sphinx: ">=4,<5"
- os: windows-latest
python-version: "3.8"
sphinx: ">=4,<5"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[linkify,testing]
pip install --upgrade-strategy "only-if-needed" "sphinx${{ matrix.sphinx }}"
- name: Run pytest
run: |
pytest --cov=myst_parser --cov-report=xml --cov-report=term-missing
coverage xml
- name: Upload to Codecov
if: github.repository == 'executablebooks/MyST-Parser' && matrix.python-version == 3.8
uses: codecov/codecov-action@v1
with:
name: myst-parser-pytests
flags: pytests
file: ./coverage.xml
fail_ci_if_error: true
check-myst-docutils:
name: Check myst-docutils install
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
docutils-version: ["0.17", "0.18", "0.19"]
steps:
- name: Checkout source
uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: "3.8"
- name: Install setup
run: |
python -m pip install --upgrade pip
pip install tomlkit
- name: Modify setup
run: python .github/workflows/docutils_setup.py pyproject.toml README.md
- name: Install dependencies
run: |
pip install .
pip install pytest~=6.2 pytest-param-files~=0.3.3 pygments docutils==${{ matrix.docutils-version }}
- name: ensure sphinx is not installed
run: |
python -c "\
try:
import sphinx
except ImportError:
pass
else:
raise AssertionError()"
- name: Run pytest for docutils-only tests
run: pytest tests/test_docutils.py tests/test_renderers/test_fixtures_docutils.py tests/test_renderers/test_include_directive.py
- name: Run docutils CLI
run: echo "test" | myst-docutils-html
publish:
name: Publish myst-parser to PyPi
needs: [pre-commit, tests, check-myst-docutils]
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: "3.8"
- name: install flit
run: |
pip install flit~=3.4
- name: Build and publish
run: |
flit publish
env:
FLIT_USERNAME: __token__
FLIT_PASSWORD: ${{ secrets.PYPI_KEY }}
publish-docutils:
name: Publish myst-docutils to PyPi
needs: [publish]
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: "3.8"
- name: install flit and tomlkit
run: |
pip install flit~=3.4 tomlkit
- name: Modify setup
run: python .github/workflows/docutils_setup.py pyproject.toml README.md
- name: Build and publish
run: |
flit publish
env:
FLIT_USERNAME: __token__
FLIT_PASSWORD: ${{ secrets.PYPI_KEY_DOCUTILS }}

135
.gitignore vendored Normal file
View File

@ -0,0 +1,135 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
!tests/**/*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
_archive/
.vscode/
.DS_Store

59
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,59 @@
# Install pre-commit hooks via
# pre-commit install
exclude: >
(?x)^(
\.vscode/settings\.json|
tests/test_commonmark/commonmark\.json|
.*\.xml|
tests/.*/.*\.md
)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-json
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v2.38.2
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.8.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies:
- flake8-comprehensions
- flake8-bugbear
# - flake8-self~=0.2.2
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
hooks:
- id: mypy
args: [--config-file=pyproject.toml]
additional_dependencies:
- sphinx~=5.0
- markdown-it-py>=1.0.0,<3.0.0
- mdit-py-plugins~=0.3.1
files: >
(?x)^(
myst_parser/.*py|
)$

14
.readthedocs.yml Normal file
View File

@ -0,0 +1,14 @@
version: 2
python:
version: "3"
install:
- method: pip
path: .
extra_requirements:
- linkify
- rtd
sphinx:
builder: html
fail_on_warning: true

728
CHANGELOG.md Normal file
View File

@ -0,0 +1,728 @@
# Changelog
## 0.18.1 - 2022-27-09
Full Changelog: [v0.18.0...v0.18.1](https://github.com/executablebooks/MyST-Parser/compare/v0.18.0...v0.18.1)
- ⬆️ UPGRADE: docutils 0.19 support (#611)
- ✨ NEW: Add `attrs_image` (experimental) extension (#620)
- e.g. `![image](image.png){#id .class width=100px}`
- See: [Optional syntax section](docs/syntax/optional.md)
- **Important**: This is an experimental extension, and may change in future releases
## 0.18.0 - 2022-06-07
Full Changelog: [v0.17.2...v0.18.0](https://github.com/executablebooks/MyST-Parser/compare/v0.17.2...v0.18.0)
This release adds support for Sphinx v5 (dropping v3), restructures the code base into modules, and also restructures the documentation, to make it easier for developers/users to follow.
It also introduces **document-level configuration** *via* the Markdown top-matter, under the `myst` key.
See the [Local configuration](docs/configuration.md) section for more information.
### Breaking changes
This should not be breaking, for general users of the sphinx extension (with `sphinx>3`),
but will be for anyone directly using the Python API, mainly just requiring changes in import module paths.
The `to_docutils`, `to_html`, `to_tokens` (from `myst_parser/main.py`) and `mock_sphinx_env`/`parse` (from `myst_parser.sphinx_renderer.py`) functions have been removed, since these were primarily for internal testing.
Instead, for single page builds, users should use the docutils parser API/CLI (see [](docs/docutils.md)),
and for testing, functionality has been moved to <https://github.com/chrisjsewell/sphinx-pytest>.
The top-level `html_meta` and `substitutions` top-matter keys have also been deprecated (i.e. they will still work but will emit a warning), as they now form part of the `myst` config, e.g.
```yaml
---
html_meta:
"description lang=en": "metadata description"
substitutions:
key1: I'm a **substitution**
---
```
is replaced by:
```yaml
---
myst:
html_meta:
"description lang=en": "metadata description"
substitutions:
key1: I'm a **substitution**
---
```
### Key PRs
- ♻️📚 Restructure code base and documentation (#566)
- ⬆️ Drop Sphinx 3 and add Sphinx 5 support (#579)
- 🐛 FIX: `parse_directive_text` when body followed by options (#580)
- 🐛 FIX: floor table column widths to integers (#568), thanks to @Jean-Abou-Samra!
## 0.17.2 - 2022-04-17
Full Changelog: [v0.17.1...v0.17.2](https://github.com/executablebooks/MyST-Parser/compare/v0.17.1...v0.17.2)
- ♻️ REFACTOR: Replace `attrs` by `dataclasses` for configuration (#557)
## 0.17.1 - 2022-04-15
Full Changelog: [v0.17.0...v0.17.1](https://github.com/executablebooks/MyST-Parser/compare/v0.17.0...v0.17.1)
- 🐛 FIX: Heading anchor resolution for parallel builds (#525)
- 🔧 MAINTAIN: Move packaging from setuptools to flit (#553)
- 🔧 MAINTAIN: Directly specify attrs dependency (#555)
## 0.17.0 - 2022-02-11
This release contains a number of breaking improvements.
Full Changelog: [v0.16.1...v0.17.0](https://github.com/executablebooks/MyST-Parser/compare/v0.16.1...v0.17.0)
### ‼️ Markdown link resolution improvements
**WARNING: This is a breaking change for links that rely on auto-generated anchor links**. You should now [manually enable auto-generated anchor links](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html?highlight=anchor#auto-generated-header-anchors) if you see errors like `WARNING reference target not found`.
Markdown links are of the format `[text](link)`.
MyST-Parser looks to smartly resolve such links, by identifying if they are:
1. A link to an external resource, e.g. `[text](http://example.com)`
2. A link to another source document, e.g. `[text](file.md)`
- If `header-anchors` are enabled, anchor links are also supported, e.g. `[text](file.md#anchor)`
3. A link to an internal sphinx cross-reference, e.g. `[text](my-reference)`
an additional situation is now supported:
4. A link to a source file, which is not a document, e.g. `[text](file.js)`. This behaves similarly to the sphinx `download` role.
In addition, configuration to more finely tune this behaviour has been added.
- `myst_all_links_external=True`, will make all links be treated as (1)
- `myst_url_schemes=("http", "https")`, sets what URL schemes are treated as (1)
- `myst_ref_domains=("std", "py")`, sets what Sphinx reference domains are checked, when handling (3)
See [Markdown Links and Referencing](docs/syntax/syntax.md#markdown-links-and-referencing) for more information.
### ‼️ Dollarmath is now disabled by default
**WARNING: This is a breaking change for dollar math**. You should now manually enable dollar math (see below).
The default configuration is now `myst_enable_extensions=()`, instead of `myst_enable_extensions=("dollarmath",)`.
If you are using math enclosed in `$` or `$$` in your documents, you should enable `dollarmath` explicitly.
See [Dollar delimited math](docs/syntax/optional.md#math-shortcuts) for more information.
### ⬆️ Drop Python 3.6 support
MyST-Parser now supports, and is tested against, Python 3.7 to 3.10.
### ✨ Add the `strikethrough` extension and `myst_gfm_only` configuration
The `strikethrough` extension allows text within `~~` delimiters to have a strike-through (horizontal line) placed over it.
For example, `~~strikethrough with *emphasis*~~` renders as: ~~strikethrough with *emphasis*~~.
**Important**: This extension is currently only supported for HTML output.
See [Strikethrough](docs/syntax/optional.md#strikethrough) for more information.
The `myst_gfm_only=True` configuration sets up specific configuration, to enable compliance only with [GitHub-flavored Markdown](https://github.github.com/gfm/), including enabling the `strikethrough`, `tasklist` and `linkify` extensions, but disabling support for roles and directives.
### ✨ Add `myst_title_to_header` configuration
Setting `myst_title_to_header=True`, allows for a `title` key in the frontmatter to be used as the document title.
for example:
```md
---
title: My Title with *emphasis*
---
```
would be equivalent to:
```md
# My Title with *emphasis*
```
See [Front matter](docs/syntax/syntax.md#front-matter) for more information.
### 👌 Internal improvements
👌 IMPROVE: Convert nested headings to rubrics.
Headings within directives are not directly supported by sphinx, since they break the structure of the document. Previously myst-parser would emit a `myst.nested_header` warning, but still generate the heading, leading to unexpected outcomes.
Now the warning is still emitted, but also the heading is rendered as a [rubric](https://docutils.sourceforge.io/docs/ref/rst/directives.html#rubric) non-structural heading (i.e. it will not show in the ToC).
Other internal improvements primarily focused in improving support for the for "docutils-only" use, introduced in `v0.16`:
- ♻️ REFACTOR: `default_parser` -> `create_md_parser` in [#474](https://github.com/executablebooks/MyST-Parser/pull/474)
- 👌 IMPROVE: Add `bullet` attribute to `bullet_list` node in [#465](https://github.com/executablebooks/MyST-Parser/pull/465)
- 👌 IMPROVE: Use correct renderer for `state.inline_text` in [#466](https://github.com/executablebooks/MyST-Parser/pull/466)
- 👌 IMPROVE: Docutils parser settings in [#476](https://github.com/executablebooks/MyST-Parser/pull/476)
- 🐛 FIX: front-matter rendering with docutils in [#477](https://github.com/executablebooks/MyST-Parser/pull/477)
- 👌 IMPROVE: Code block highlighting in [#478](https://github.com/executablebooks/MyST-Parser/pull/478)
- 👌 IMPROVE: `note_refname` for docutils internal links in [#481](https://github.com/executablebooks/MyST-Parser/pull/481)
- 🐛 FIX: Ordered list starting number in [#483](https://github.com/executablebooks/MyST-Parser/pull/483)
- 👌 IMPROVE: Propagate enumerated list suffix in [#484](https://github.com/executablebooks/MyST-Parser/pull/484)
- 👌 IMPROVE: `DocutilsRenderer.create_highlighted_code_block` in [#488](https://github.com/executablebooks/MyST-Parser/pull/488)
- 🐛 FIX: Source line reporting for nested parsing in [#490](https://github.com/executablebooks/MyST-Parser/pull/490)
- 🔧 MAINTAIN: Implement `MockInliner.parse` in [#504](https://github.com/executablebooks/MyST-Parser/pull/504)
## 0.16.1 - 2021-12-16
✨ NEW: Add `myst_linkify_fuzzy_links` option.
When using the [`linkify` extension](docs/syntax/optional.md#linkify), this option can be used to disable matching of links that do not contain a schema (such as `http://`).
## 0.16.0 - 2021-12-06
This release contains a number of exciting improvements:
### Upgrade of Markdown parser
`markdown-it-py` has been upgraded to [v2.0.0](https://github.com/executablebooks/markdown-it-py/releases/tag/v2.0.0).
This upgrade brings full compliance with the [CommonMark v0.30 specification](https://spec.commonmark.org/0.30/).
Additionally, `mdit-py-plugins` has been upgraded to [v0.3.0](https://github.com/executablebooks/mdit-py-plugins/releases/tag/v0.3.0).
This improves the parsing of the MyST target syntax, to allow for spaces and additional special characters in the target name,
for example this is now valid:
```md
(a bc |@<>*./_-+:)=
# Header
```
Also MyST role syntax now supports unlimited length in the role name and new lines in the content.
For example, this is now valid:
```md
{abc}`xy
new line`
```
### Improvements for Docutils-only use
MyST now allows for Docutils-only use (outside of Sphinx), that allows for MyST configuration options to be set via the `docutils.conf` file, or on the command line.
On installing MyST-Parser, the following CLI-commands are made available:
- `myst-docutils-html`: converts MyST to HTML
- `myst-docutils-html5`: converts MyST to HTML5
- `myst-docutils-latex`: converts MyST to LaTeX
- `myst-docutils-xml`: converts MyST to docutils-native XML
- `myst-docutils-pseudoxml`: converts MyST to pseudo-XML (to visualise the AST structure)
You can also install the [myst-docutils](https://pypi.org/project/myst-docutils/) package from `pip`,
which includes no direct install requirements on docutils or sphinx.
See [MyST with Docutils](docs/docutils.md) for more information.
Thanks to help from [@cpitclaudel](https://github.com/cpitclaudel)!
### Include MyST files in RST files
With `docutils>=0.17`, the `include` directive has a `parser` option.
This can be used with myst-parser to include MyST files in RST files.
```md
Parse using the docutils only parser:
.. include:: include.md
:parser: myst_parser.docutils_
Parse using the sphinx parser:
.. include:: include.md
:parser: myst_parser.sphinx_
```
### Addition of the `fieldlist` syntax extension
Field lists are mappings from field names to field bodies, based on the [reStructureText syntax](https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#field-lists):
```rst
:name only:
:name: body
:name:
Multiple
Paragraphs
```
This should eventually allow for MyST Markdown docstrings! (see <https://github.com/executablebooks/MyST-Parser/issues/228>)
See [Field Lists syntax](docs/syntax/optional.md#field-lists) for more information.
### Improvements to table rendering
Tables with no body are now allowed, for example:
```md
| abc | def |
| --- | --- |
```
Also cell alignment HTML classes have now been changed to: `text-left`, `text-center`, or `text-right`, for example:
```md
| left | center | right |
| :--- | :----: | ----: |
| a | b | c |
```
is converted to:
```html
<table class="colwidths-auto">
<thead>
<tr>
<th class="text-left head"><p>left</p></th>
<th class="text-center head"><p>center</p></th>
<th class="text-right head"><p>right</p></th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-left"><p>a</p></td>
<td class="text-center"><p>b</p></td>
<td class="text-right"><p>c</p></td>
</tr>
</tbody>
</table>
```
These classes should be supported by most sphinx HTML themes.
See [Tables syntax](docs/syntax/syntax.md#tables) for more information.
### Pull Requests
- 🐛 FIX: Add mandatory attributes on `enumerated_list` by @cpitclaudel in [#418](https://github.com/executablebooks/MyST-Parser/pull/418)
- 📚 DOCS: Add reference to MySTyc in landing page by @astrojuanlu in [#413](https://github.com/executablebooks/MyST-Parser/pull/413)
- ⬆️ UPGRADE: markdown-it-py v2, mdit-py-plugins v0.3 by @chrisjsewell in [#449](https://github.com/executablebooks/MyST-Parser/pull/449)
- 👌 IMPROVE: Table rendering by @chrisjsewell in [#450](https://github.com/executablebooks/MyST-Parser/pull/450)
- 🐛 FIX: Ensure parent files are re-built if `include` file changes by @chrisjsewell in [#451](https://github.com/executablebooks/MyST-Parser/pull/451)
- 🐛 FIX: Convert empty directive option to `None` by @chrisjsewell in [#452](https://github.com/executablebooks/MyST-Parser/pull/452)
- 👌 IMPROVE: Add `\\` for hard-breaks in latex by @chrisjsewell in [#453](https://github.com/executablebooks/MyST-Parser/pull/453)
- 🔧 MAINTAIN: Remove empty "sphinx" extra by @hukkin in [#350](https://github.com/executablebooks/MyST-Parser/pull/350)
- ✨ NEW: Add `fieldlist` extension by @chrisjsewell in [#455](https://github.com/executablebooks/MyST-Parser/pull/455)
- ✨ NEW: Add Docutils MyST config and CLI by @cpitclaudel in [#426](https://github.com/executablebooks/MyST-Parser/pull/426)
- 🔧 MAINTAIN: Add publishing job for `myst-docutils` by @chrisjsewell in [#456](https://github.com/executablebooks/MyST-Parser/pull/456)
- 🧪 TESTS: Add for `gettext_additional_targets` by @jpmckinney in [#459](https://github.com/executablebooks/MyST-Parser/pull/459)
### New Contributors
- @cpitclaudel made their first contribution in [#418](https://github.com/executablebooks/MyST-Parser/pull/418)
- @astrojuanlu made their first contribution in [#413](https://github.com/executablebooks/MyST-Parser/pull/413)
**Full Changelog**: <https://github.com/executablebooks/MyST-Parser/compare/v0.15.2...v0.16.0>
## 0.15.2 - 2021-08-26
This is mainly a maintenance release that fixes some incompatibilities with `sphinx<3.1`, improvements for compatibility
with `docutils=0.17`, and improvements to robustness.
## 0.15.1 - 2021-06-18
👌 IMPROVE: MathJax compatibility with `nbsphinx`
`nbsphinx` also overrides the MathJax configuration.
For compatibility, `output_area` is added to the list of default processed classes, and the override warning is allowed to be suppressed with `suppress_warnings = ["myst.mathjax"]`.
## 0.15.0 - 2021-06-13
### Upgraded to `sphinx` v4 ⬆️
A principe change in this release is to updates the requirements of myst-parser from `sphinx>=2,<4` to `sphinx>=3,<5`.
### Changed MathJax handling ♻️
Instead of removing all `$` processing for the whole project,
during MyST document parsing, the top-level section is now given the classes `tex2jax_ignore` and `mathjax_ignore` (turning off default MathJax processing of all HTML elements)
and MathJax is then configured to process elements with the `tex2jax_process|mathjax_process|math` classes.
See [the math syntax guide](docs/syntax/optional.md#math-shortcuts) for further information.
### Set URL scheme defaults ‼️
The `myst_url_schemes` default is now: `("http", "https", "mailto", "ftp")`.
This means that only these URL will be considered as external (e.g. `[](https://example.com)`),
and references like `[](prefix:main)` will be considered as internal references.
Set `myst_url_schemes = None`, to revert to the previous default.
### Added `myst_heading_slug_func` option 👌
Use this option to specify a custom function to auto-generate heading anchors (see [Auto-generated header anchors](docs/syntax/optional.md#auto-generated-header-anchors)).
Thanks to [@jpmckinney](https://github.com/jpmckinney)!
## 0.14.0 - 2021-05-04
### Upgrade to `markdown-it-py` v1.0 ⬆️
This release updates the code-base to fully support the [markdown-it-py](https://markdown-it-py.readthedocs.io) `v1.0.0` release.
In particular for users, this update alters the parsing of tables to be consistent with the [Github Flavoured Markdown (GFM) specification](https://github.github.com/gfm/#tables-extension-).
### New Features ✨
- **Task lists** utilise the [markdown-it-py tasklists plugin](markdown_it:md/plugins), and are applied to Markdown list items starting with `[ ]` or `[x]`.
```markdown
- [ ] An item that needs doing
- [x] An item that is complete
```
Add "tasklist" to the `myst_enable_extensions` configuration to enable.
See [the optional syntax guide](docs/syntax/optional.md#task-lists) for further information.
- The **`sub-ref`** role has been added for use identical to ReST's `|name|` syntax.
This allows one to access Sphinx's built-in `|today|`, `|release|` and `|version|` substitutions, and also introduces two new substitutions: `wordcount-words` and `wordcount-minutes`, computed by the markdown-it-py [`wordcount_plugin`](https://github.com/executablebooks/mdit-py-plugins/pull/20).
```markdown
> {sub-ref}`today` | {sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
```
See [the roles syntax guide](docs/syntax/syntax.md) for further information.
- The **`dmath_double_inline`** configuration option allows display math (i.e. `$$`) within an inline context.
See [the math syntax guide](docs/syntax/optional.md#math-shortcuts) for further information.
### Remove v0.13 deprecations ‼️
The deprecations made to extension configurations and colon fences in `0.13.0` (see below) have now been removed:
- Configuration variables: `myst_admonition_enable`, `myst_figure_enable`, `myst_dmath_enable`, `myst_amsmath_enable`, `myst_deflist_enable`, `myst_html_img_enable`
- `:::{admonition,class}` -> `:::{admonition}\n:class: class`
- `:::{figure}` -> `:::{figure-md}`
### Fix extraction of nested footnotes 🐛
Previously footnote definitions in block elements like lists would crash the parsing:
```markdown
- [^e]: footnote definition in a block element
```
These are now correctly extracted.
## 0.13.7 - 2021-04-25
👌 IMPROVE: Add warning for nested headers:
Nested headers are not supported within most elements (this is a limitation of the docutils/sphinx document structure), and can lead to unexpected outcomes.
For example in admonitions:
````markdown
```{note}
# Unsupported Header
```
````
A warning (of type `myst.nested_header`) is now emitted when this occurs.
🔧 MAINTAIN: Python 3.9 is now officially supported.
## 0.13.6 - 2021-04-10
🐛 FIX: docutils `v0.17` compatibility
## 0.13.5 - 2021-02-15
- ⬆️ UPGRADE: required markdown-it-py to `v0.6.2`:
In particular, this fixes missing source line mappings for table rows and their children
- 👌 IMPROVE: Store `rawtext` in AST nodes:
We now ensure that the raw text is propagated from the Markdown tokens to the Sphinx AST.
In particular, this is required by the `gettext` builder, to generate translation POT templates.
Thanks to [@jpmckinney](https://github.com/jpmckinney)!
- ✨ NEW: Add warning types `myst.subtype`:
All parsing warnings are assigned a type/subtype, and also the messages are appended with them.
These warning types can be suppressed with the sphinx `suppress_warnings` config option.
See [How-to suppress warnings](howto/warnings) for more information.
## 0.13.3 - 2021-01-20
Minor fixes:
- 🐛 FIX: front-matter parsing for bibliographic keys
- 🐛 FIX: directive/role name translations
- 👌 IMPROVE: Add warning for multiple footnote definitions
## 0.13.2 - 2021-01-20
✨ NEW: Add `html_admonition` extension
: By adding `"html_admonition"` to `myst_enable_extensions`, you can enable parsing of `<div class="admonition">` HTML blocks to sphinx admonitions.
: This is helpful when you care about viewing the "source" Markdown, such as in Jupyter Notebooks.
: For example:
```html
<div class="admonition note" name="html-admonition">
<p class="title">This is the **title**</p>
This is the *content*
</div>
```
: See [the optional syntax guide](docs/syntax/optional.md) for further information.
👌 IMPROVE: Footnotes
: If the label is an integer, then it will always use this integer for the rendered label (i.e. they are manually numbered).
: Add `myst_footnote_transition` configuration, to turn on/off transition line.
: Add `footnotes` class to transition `<hr>` in HTML.
: See [the syntax guide](docs/syntax/syntax.md) for further information.
👌 IMPROVE: `substitution` extension logic
: Parse inline substitutions without block rules, unless the substitution starts with a directive.
🐛 FIX: Render front-matter as `field_list`
: To improve use by sphinx extensions).
👌 IMPROVE: Code quality
: Add isort and mypy type checking to code base.
(thanks to contributors @akhmerov, @tfiers)
## 0.13.1 - 2020-12-31
👌 Directives can now be used for inline substitutions, e.g.
```md
---
substitutions:
key: |
```{image} img/fun-fish.png
:alt: fishy
:height: 20px
```
---
An inline image: {{ key }}
```
## 0.13.0 - 2020-12-18
This release makes some major updates to the optional syntaxes.
For full details see [Optional MyST Syntaxes](docs/syntax/optional.md).
### 🗑 Deprecations
`myst_enable_extensions = ["dollarmath", ...]` now replaces and deprecates individual enable configuration variables: `admonition_enable` -> `"colon_fence"`, `figure_enable` -> `"colon_fence"`, `dmath_enable` -> `"dollarmath"`, `amsmath` -> `"colon_fence"`, `deflist_enable` -> `"deflist"`, `html_img_enable` -> `"html_image"`.
The `colon_fence` extension (replacing `admonition_enable`) now works exactly the same as normal ```` ``` ```` code fences, but using `:::` delimiters. This is helpful for directives that contain Markdown text, for example:
```md
:::{admonition} The title
:class: note
This note contains *Markdown*
:::
```
### ✨ New
The `smartquotes` extension will automatically convert standard quotations to their opening/closing variants:
- `'single quotes'`: single quotes
- `"double quotes"`: “double quotes”
The `linkify` extension will automatically identify “bare” web URLs, like `www.example.com`, and add hyperlinks; www.example.com.
This extension requires that [linkify-it-py](https://github.com/tsutsu3/linkify-it-py) is installed.
The `replacements` extension will automatically convert some common typographic texts, such as `+-` -> `±`.
The `substitution` extension allows you to specify "substitution definitions" in either the `conf.py` (as `myst_substitutions`) and/or individual file's front-matter (front-matter takes precedence), which will then replace substitution references. For example:
```md
---
substitutions:
key1: definition
---
{{ key1 }}
```
The substitutions are assessed as [jinja2 expressions](http://jinja.palletsprojects.com/) and includes the [Sphinx Environment](https://www.sphinx-doc.org/en/master/extdev/envapi.html) as `env`, so you can do powerful thinks like:
```
{{ [key1, env.docname] | join('/') }}
```
The `figure-md` directive has been added (replacing `enable_figure`), which parses a "Markdown friendly" figure (used with the `colon_fence` extension):
```md
:::{figure-md} fig-target
:class: myclass
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="200px">
This is a caption in **Markdown**
:::
```
### 👌 Improvements
Using the `html_image` extension, HTML images are now processed for both blocks and (now) inline.
So you can correctly do, for example:
```md
Im an inline image: <img src="img/fun-fish.png" height="20px">
| table column |
| ----------------------------------------- |
| <img src="img/fun-fish.png" width="20px"> |
```
## 0.12.10 - 2020-09-21
🐛 FIX: allow dates to be parsed in frontmatter.
: This fixes a bug that would raise errors at parse time if non-string date objects were in front-matter YAML. See [#253](https://github.com/executablebooks/MyST-Parser/pull/253)
## 0.12.9 - 2020-09-08
✨ NEW: Auto-generate heading anchors.
: This utilises `markdown-it-py`'s `anchors-plugin`, to generate unique anchor "slugs" for each header (up to a certain level),
and allows them to be referenced *via* a relative path, e.g. `[](./file.md#header-anchor)`, or in the same document, e.g. `[](#header-anchor)`.
Slugs are generated in the GitHub style ([see here](https://github.com/Flet/github-slugger)); lower-case text, removing punctuation, replacing spaces with `-`, enforce uniqueness *via* suffix enumeration `-1`.
It is enabled in your `conf.py` *via* `myst_heading_anchors = 2` (sets maximum heading level).
See [the documentation here](docs/syntax/optional.md#auto-generated-header-anchors).
🐛 FIX: doc reference resolution for singlehtml/latex.
: These reference resolutions are passed to the "missing-reference" event, and require the `node["refdoc"]` attribute to be available, which was missing for `[text](./path/to/file.md)` type references.
## 0.12.7 - 2020-08-31
✨ NEW: Want to include your README.md in the documentation?
: See [including a file from outside the docs folder](howto/include-readme).
(👌 added `relative-docs` option in 0.12.8)
## 0.12.5 - 2020-08-28
✨ NEW: Add Markdown figure syntax
: Setting `myst_figure_enable = True` in your sphinx `conf.py`, combines the above two extended syntaxes,
to create a fully Markdown compliant version of the `figure` directive.
See [Markdown Figures](docs/syntax/optional.md#markdown-figures) for details.
(👌 formatting of caption improved in 0.12.6)
## 0.12.4 - 2020-08-27
👌 IMPROVE: the mathjax extension is now only overridden if strictly necessary (to support dollar and ams math), and the override is more precise, to mitigate any unwanted side-effects
## 0.12.3 - 2020-08-26
✨ NEW: Add definition lists.
: This addition, enabled by `myst_deflist_enable = True`, allows for "Pandoc style" definition lists to be parsed and rendered, e.g.
```md
Term 1
: Definition
```
See the [Definition Lists documentation](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#definition-lists) for further details.
👌 IMPROVE: mathjax_config override.
: Only `mathjax_config["tex2jax"]` will now be overridden, in order to not interfere with other user configurations, such as adding TeX macros.
The configuration name has also changed from `myst_override_mathjax` to `myst_update_mathjax`.
See [Mathjax and math parsing](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#mathjax-and-math-parsing) for further details.
## 0.12.2 - 2020-08-25
✨ NEW: Add the `eval-rst` directive
: This directive parses its contents as ReStructuredText, which integrates back into the rest of the document, e.g. for cross-referencing. See [this documentation](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#how-directives-parse-content) for further explanation.
In particular, this addition solves some outstanding user requests:
- How-to [include rST files into a Markdown file](https://myst-parser.readthedocs.io/en/latest/using/howto.html#include-rst-files-into-a-markdown-file)
- How-to [Use sphinx.ext.autodoc in Markdown files](https://myst-parser.readthedocs.io/en/latest/using/howto.html#use-sphinx-ext-autodoc-in-markdown-files)
Thanks to [@stephenroller](https://github.com/stephenroller) for the contribution 🎉
## 0.12.1 - 2020-08-19
✨ NEW: Add `myst_commonmark_only` config option, for restricting the parser to strict CommonMark (no extensions).
## 0.12.0 - 2020-08-19
### ‼️ BREAKING
If you are using math in your documents, be sure to read the updated [Math syntax guide](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#math-shortcuts)!
In particular, the Mathjax configuration is now overridden, such that LaTeX environments will only be rendered if `myst_amsmath_enable=True` is set.
The `myst_math_delimiters` option has also been removed (please open an issue if you would like brackets math parsing to be re-implemented).
In addition the `myst_html_img` option name has been changed to `myst_html_img_enable`.
Some underlying code has also been refactored, to centralise handling of configuration options (see [commit 98573b9](https://github.com/executablebooks/MyST-Parser/commit/98573b9c6e3602ab31d627b5266ae5c1ba2c9e5f)).
### Improved 👌
More configuration options for math parsing (see [MyST configuration options](https://myst-parser.readthedocs.io/en/latest/using/intro.html#myst-configuration-options)).
## 0.11.2 - 2020-07-13
### Added ✨
- `<img src="file.png" width="200px">` tag parsing to sphinx representation, see [the image syntax guide](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#images)
### Improved 👌
- `[title](link)` syntax now works with intersphinx references.
Recognised URI schemas can also be configured, see the [configuration options](https://myst-parser.readthedocs.io/en/latest/using/intro.html#myst-configuration-options)
## 0.11.1 - 2020-07-12
### Fix
- Correctly pin required minimum markdown-it-py version
## 0.11.0 - 2020-07-12
### Added ✨
* Special admonition directive syntax (optional):
```md
:::{note}
This text is **standard** _Markdown_
:::
```
See [the syntax guide section](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#admonition-directives-special-syntax-optional) for details.
* Direct parsing of [amsmath](https://ctan.org/pkg/amsmath) LaTeX equations (optional).
See [the syntax guide section](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#direct-latex-math-optional) for details.
### Breaking ‼️
* Sphinx configuration options are now set as separate variables, rather than a single dict.
See [MyST configuration options](https://myst-parser.readthedocs.io/en/latest/using/intro.html#myst-configuration-options) for details.
## 0.10.0 - 2020-07-08
([full changelog](https://github.com/executablebooks/MyST-Parser/compare/v0.9.1...aaed58808af485c29bbbf73c5aac10697bfa08b9))
### Improved 👌
* Support Sphinx version 3 [#197](https://github.com/executablebooks/MyST-Parser/pull/197) ([@chrisjsewell](https://github.com/chrisjsewell))
* Update Trove Classifiers [#192](https://github.com/executablebooks/MyST-Parser/pull/192) ([@chrisjsewell](https://github.com/chrisjsewell))
* Add functionality to use docutils specialized role [#189](https://github.com/executablebooks/MyST-Parser/pull/189) ([@chrisjsewell](https://github.com/chrisjsewell))
### Contributors to this release
([GitHub contributors page for this release](https://github.com/executablebooks/MyST-Parser/graphs/contributors?from=2020-07-20&to=2020-08-07&type=c))
[@AakashGfude](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3AAakashGfude+updated%3A2020-07-20..2020-08-07&type=Issues) | [@asmeurer](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Aasmeurer+updated%3A2020-07-20..2020-08-07&type=Issues) | [@choldgraf](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Acholdgraf+updated%3A2020-07-20..2020-08-07&type=Issues) | [@chrisjsewell](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Achrisjsewell+updated%3A2020-07-20..2020-08-07&type=Issues) | [@codecov](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Acodecov+updated%3A2020-07-20..2020-08-07&type=Issues) | [@webknjaz](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Awebknjaz+updated%3A2020-07-20..2020-08-07&type=Issues) | [@welcome](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Awelcome+updated%3A2020-07-20..2020-08-07&type=Issues)
## Past Releases
### Contributors
([GitHub contributors page for these releases](https://github.com/executablebooks/MyST-Parser/graphs/contributors?from=2020-01-01&to=2020-07-20&type=c))
[@akhmerov](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Aakhmerov+updated%3A2020-01-01..2020-07-20&type=Issues) | [@asmeurer](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Aasmeurer+updated%3A2020-01-01..2020-07-20&type=Issues) | [@certik](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Acertik+updated%3A2020-01-01..2020-07-20&type=Issues) | [@choldgraf](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Acholdgraf+updated%3A2020-01-01..2020-07-20&type=Issues) | [@chrisjsewell](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Achrisjsewell+updated%3A2020-01-01..2020-07-20&type=Issues) | [@codecov](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Acodecov+updated%3A2020-01-01..2020-07-20&type=Issues) | [@dhermes](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Adhermes+updated%3A2020-01-01..2020-07-20&type=Issues) | [@filippo82](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Afilippo82+updated%3A2020-01-01..2020-07-20&type=Issues) | [@jlperla](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Ajlperla+updated%3A2020-01-01..2020-07-20&type=Issues) | [@jstac](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Ajstac+updated%3A2020-01-01..2020-07-20&type=Issues) | [@martinagvilas](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Amartinagvilas+updated%3A2020-01-01..2020-07-20&type=Issues) | [@mlncn](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Amlncn+updated%3A2020-01-01..2020-07-20&type=Issues) | [@mmcky](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Ammcky+updated%3A2020-01-01..2020-07-20&type=Issues) | [@moorepants](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Amoorepants+updated%3A2020-01-01..2020-07-20&type=Issues) | [@najuzilu](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Anajuzilu+updated%3A2020-01-01..2020-07-20&type=Issues) | [@nathancarter](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Anathancarter+updated%3A2020-01-01..2020-07-20&type=Issues) | [@pauleveritt](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Apauleveritt+updated%3A2020-01-01..2020-07-20&type=Issues) | [@phaustin](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Aphaustin+updated%3A2020-01-01..2020-07-20&type=Issues) | [@rossbar](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Arossbar+updated%3A2020-01-01..2020-07-20&type=Issues) | [@rowanc1](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Arowanc1+updated%3A2020-01-01..2020-07-20&type=Issues) | [@sbliven](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Asbliven+updated%3A2020-01-01..2020-07-20&type=Issues) | [@webknjaz](https://github.com/search?q=repo%3Aexecutablebooks%2FMyST-Parser+involves%3Awebknjaz+updated%3A2020-01-01..2020-07-20&type=Issues)

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 ExecutableBookProject
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# MyST-Parser
[![Github-CI][github-ci]][github-link]
[![Coverage Status][codecov-badge]][codecov-link]
[![Documentation Status][rtd-badge]][rtd-link]
[![Code style: black][black-badge]][black-link]
[![PyPI][pypi-badge]][pypi-link]
[![Conda][conda-badge]][conda-link]
[![PyPI - Downloads][install-badge]][install-link]
**MyST is a rich and extensible flavor of Markdown meant for technical documentation and publishing**.
MyST is a flavor of markdown that is designed for simplicity, flexibility, and extensibility.
This repository serves as the reference implementation of MyST Markdown, as well as a collection of tools to support working with MyST in Python and Sphinx.
It contains an extended [CommonMark](https://commonmark.org)-compliant parser using [`markdown-it-py`](https://markdown-it-py.readthedocs.io/), as well as a [Sphinx](https://www.sphinx-doc.org) extension that allows you to write MyST Markdown in Sphinx.
[**See the MyST Parser documentation for more information**](https://myst-parser.readthedocs.io/en/latest/).
## Installation
To install the MyST parser, run the following in a
[Conda environment](https://docs.conda.io) (recommended):
```bash
conda install -c conda-forge myst-parser
```
or
```bash
pip install myst-parser
```
Or for package development:
```bash
git clone https://github.com/executablebooks/MyST-Parser
cd MyST-Parser
git checkout master
pip install -e .[code_style,testing,rtd]
```
To use the MyST parser in Sphinx, simply add: `extensions = ["myst_parser"]` to your `conf.py`.
## Contributing
We welcome all contributions!
See the [Contributing Guide](https://myst-parser.readthedocs.io/en/latest/develop/index.html) for more details.
[github-ci]: https://github.com/executablebooks/MyST-Parser/workflows/continuous-integration/badge.svg?branch=master
[github-link]: https://github.com/executablebooks/MyST-Parser
[codecov-badge]: https://codecov.io/gh/executablebooks/MyST-Parser/branch/master/graph/badge.svg
[codecov-link]: https://codecov.io/gh/executablebooks/MyST-Parser
[rtd-badge]: https://readthedocs.org/projects/myst-parser/badge/?version=latest
[rtd-link]: https://myst-parser.readthedocs.io/en/latest/?badge=latest
[black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg
[pypi-badge]: https://img.shields.io/pypi/v/myst-parser.svg
[pypi-link]: https://pypi.org/project/myst-parser
[conda-badge]: https://anaconda.org/conda-forge/myst-parser/badges/version.svg
[conda-link]: https://anaconda.org/conda-forge/myst-parser
[black-link]: https://github.com/ambv/black
[install-badge]: https://img.shields.io/pypi/dw/myst-parser?label=pypi%20installs
[install-link]: https://pypistats.org/packages/myst-parser

10
codecov.yml Normal file
View File

@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: 90%
threshold: 0.5%
patch:
default:
target: 75%
threshold: 0.5%

2
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
_build/
_api/

27
docs/Makefile Normal file
View File

@ -0,0 +1,27 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# raise warnings to errors
html-strict:
@$(SPHINXBUILD) -b html -nW --keep-going "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
clean:
rm -r $(BUILDDIR)

24
docs/_static/custom.css vendored Normal file
View File

@ -0,0 +1,24 @@
/** Add a counter before subsections **/
h1 {
counter-reset: subsection;
text-decoration: underline;
}
h2 {
counter-reset: subsubsection;
}
h2::before {
counter-increment: subsection;
content: counter(subsection) ". ";
}
h3::before {
counter-increment: subsubsection;
content: counter(subsection) "." counter(subsubsection) ". ";
}
/** No icon for admonitions with no-icon class */
.admonition > .admonition-title, div.admonition.no-icon > .admonition-title::before {
content: "";
}
.admonition > .admonition-title, div.admonition.no-icon > .admonition-title {
padding-left: .6rem;
}

1
docs/_static/logo-square.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.78 73.9"><defs><style>.cls-1{fill:#616161;}.cls-2{fill:#f37726;}</style></defs><title>Artboard 4</title><path class="cls-1" d="M14.66,41.7V38.09l4.67-.79V4.44l-4.67-.8V0h12.2L40.3,31.66h.17L53.47,0H66.05V3.64l-4.67.8V37.3l4.67.79V41.7h-15V38.09L56,37.3V7.7l-.15,0L42.3,40.22H38.55l-14-32.49-.14,0,.18,17.43V37.3l5.05.79V41.7Z"/><path class="cls-2" d="M37.88,73.9q0-8.82-10.13-8.82H19.33q-7.83,0-12.53-3.79T0,50.25l4.8-1.63q2.7,8.46,13.78,8.75h9.17q9.63,0,13.13,7.53Q44.3,57.37,54,57.37h8.45q11.76,0,14.55-8.72l4.78,1.6Q79.71,57.61,75,61.33T62.63,65.08H53.88q-10,0-10,8.82"/></svg>

After

Width:  |  Height:  |  Size: 670 B

1
docs/_static/logo-wide.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

96
docs/api/reference.rst Normal file
View File

@ -0,0 +1,96 @@
.. _api/main:
==========
Python API
==========
Source text parsers
-------------------
.. _api/docutils_parser:
Docutils
........
.. autoclass:: myst_parser.docutils_.Parser
:members: parse
:undoc-members:
:member-order: bysource
:show-inheritance:
.. _api/sphinx_parser:
Sphinx
......
.. autoclass:: myst_parser.parsers.sphinx_.MystParser
:members: supported, parse
:undoc-members:
:member-order: bysource
:show-inheritance:
:exclude-members: __init__
.. _api/renderers:
Markdown-it to docutils
-----------------------
These renderers take the markdown-it parsed token stream and convert it to
the docutils AST. The sphinx renderer is a subclass of the docutils one,
with some additional methods only available *via* sphinx e.g. multi-document cross-referencing.
Docutils
........
.. autoclass:: myst_parser.mdit_to_docutils.base.DocutilsRenderer
:special-members: __output__, __init__
:members: render, nested_render_text, add_line_and_source_path, current_node_context
:undoc-members:
:member-order: bysource
:show-inheritance:
Sphinx
......
.. autoclass:: myst_parser.mdit_to_docutils.sphinx_.SphinxRenderer
:special-members: __output__
:members: render_internal_link, render_math_block_label
:undoc-members:
:member-order: alphabetical
:show-inheritance:
.. _api/directive:
Directive and role processing
-----------------------------
This module processes the content of a directive:
.. automodule:: myst_parser.parsers.directives
:members:
These classes are parsed to sphinx roles and directives,
to mimic the original docutls rST specific parser elements,
but instead run nested parsing with the markdown parser.
.. autoclass:: myst_parser.mocking.MockInliner
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: myst_parser.mocking.MockState
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: myst_parser.mocking.MockStateMachine
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: myst_parser.mocking.MockIncludeDirective
:members:
:undoc-members:
:show-inheritance:

169
docs/conf.py Normal file
View File

@ -0,0 +1,169 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
from datetime import date
from sphinx.application import Sphinx
from myst_parser import __version__
# -- Project information -----------------------------------------------------
project = "MyST Parser"
copyright = f"{date.today().year}, Executable Book Project"
author = "Executable Book Project"
version = __version__
master_doc = "index"
language = "en"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"myst_parser",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx_design",
"sphinxext.rediraffe",
"sphinxcontrib.mermaid",
"sphinxext.opengraph",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_book_theme"
html_logo = "_static/logo-wide.svg"
html_favicon = "_static/logo-square.svg"
html_title = ""
html_theme_options = {
"home_page_in_toc": True,
"github_url": "https://github.com/executablebooks/MyST-Parser",
"repository_url": "https://github.com/executablebooks/MyST-Parser",
"repository_branch": "master",
"path_to_docs": "docs",
"use_repository_button": True,
"use_edit_page_button": True,
}
# OpenGraph metadata
ogp_site_url = "https://myst-parser.readthedocs.io/en/latest"
# This is the image that GitHub stores for our social media previews
ogp_image = "https://repository-images.githubusercontent.com/240151150/316bc480-cc23-11eb-96fc-4ab2f981a65d" # noqa: E501
ogp_custom_meta_tags = [
'<meta name="twitter:card" content="summary_large_image">',
]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
myst_enable_extensions = [
"dollarmath",
"amsmath",
"deflist",
"fieldlist",
"html_admonition",
"html_image",
"colon_fence",
"smartquotes",
"replacements",
"linkify",
"strikethrough",
"substitution",
"tasklist",
"attrs_image",
]
myst_number_code_blocks = ["typescript"]
myst_heading_anchors = 2
myst_footnote_transition = True
myst_dmath_double_inline = True
rediraffe_redirects = {
"using/intro.md": "sphinx/intro.md",
"sphinx/intro.md": "intro.md",
"using/use_api.md": "api/index.md",
"api/index.md": "api/reference.rst",
"using/syntax.md": "syntax/syntax.md",
"using/syntax-optional.md": "syntax/optional.md",
"using/reference.md": "syntax/reference.md",
"sphinx/reference.md": "configuration.md",
"sphinx/index.md": "faq/index.md",
"sphinx/use.md": "faq/index.md",
"sphinx/faq.md": "faq/index.md",
"explain/index.md": "develop/background.md",
}
suppress_warnings = ["myst.strikethrough"]
intersphinx_mapping = {
"python": ("https://docs.python.org/3.7", None),
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
"markdown_it": ("https://markdown-it-py.readthedocs.io/en/latest", None),
}
# autodoc_default_options = {
# "show-inheritance": True,
# "special-members": "__init__, __enter__, __exit__",
# "members": True,
# # 'exclude-members': '',
# "undoc-members": True,
# # 'inherited-members': True
# }
autodoc_member_order = "bysource"
nitpicky = True
nitpick_ignore = [
("py:class", "docutils.nodes.document"),
("py:class", "docutils.nodes.docinfo"),
("py:class", "docutils.nodes.Element"),
("py:class", "docutils.nodes.Node"),
("py:class", "docutils.nodes.field_list"),
("py:class", "docutils.nodes.problematic"),
("py:class", "docutils.nodes.pending"),
("py:class", "docutils.nodes.system_message"),
("py:class", "docutils.statemachine.StringList"),
("py:class", "docutils.parsers.rst.directives.misc.Include"),
("py:class", "docutils.parsers.rst.Parser"),
("py:class", "docutils.utils.Reporter"),
("py:class", "nodes.Element"),
("py:class", "nodes.Node"),
("py:class", "nodes.system_message"),
("py:class", "Directive"),
("py:class", "Include"),
("py:class", "StringList"),
("py:class", "DocutilsRenderer"),
("py:class", "MockStateMachine"),
]
def setup(app: Sphinx):
"""Add functions to the Sphinx setup."""
from myst_parser._docs import (
DirectiveDoc,
DocutilsCliHelpDirective,
MystConfigDirective,
)
app.add_css_file("custom.css")
app.add_directive("myst-config", MystConfigDirective)
app.add_directive("docutils-cli-help", DocutilsCliHelpDirective)
app.add_directive("doc-directive", DirectiveDoc)

106
docs/configuration.md Normal file
View File

@ -0,0 +1,106 @@
(sphinx/config-options)=
# Configuration
MyST parsing can be configured at both the global and individual document level,
with the most specific configuration taking precedence.
## Global configuration
Overriding the default configuration at the global level is achieved by specifying variables in the Sphinx `conf.py` file.
All `myst_parser` global configuration variables are prefixed with `myst_`, e.g.
```python
myst_enable_extensions = ["deflist"]
```
:::{seealso}
Configuration in Docutils, in the [](docutils.md) section.
:::
```{myst-config}
:sphinx:
:scope: global
```
### Extensions
Configuration specific to syntax extensions:
```{myst-config}
:sphinx:
:extensions:
:scope: global
```
## Local configuration
```{versionadded} 0.18
```
The following configuration variables are available at the document level.
These can be set in the document [front matter](syntax/frontmatter), under the `myst` key, e.g.
```yaml
---
myst:
enable_extensions: ["deflist"]
---
```
```{myst-config}
:sphinx:
:scope: local
```
### Extensions
Configuration specific to syntax extensions:
```{myst-config}
:sphinx:
:extensions:
:scope: local
```
## List of syntax extensions
Full details in the [](syntax/extensions) section.
amsmath
: enable direct parsing of [amsmath](https://ctan.org/pkg/amsmath) LaTeX equations
colon_fence
: Enable code fences using `:::` delimiters, [see here](syntax/colon_fence) for details
deflist
: Enable definition lists, [see here](syntax/definition-lists) for details
dollarmath
: Enable parsing of dollar `$` and `$$` encapsulated math
fieldlist
: Enable field lists, [see here](syntax/fieldlists) for details
html_admonition
: Convert `<div class="admonition">` elements to sphinx admonition nodes, see the [HTML admonition syntax](syntax/html-admonition) for details
html_image
: Convert HTML `<img>` elements to sphinx image nodes, [see here](syntax/images) for details
linkify
: Automatically identify "bare" web URLs and add hyperlinks
replacements
: Automatically convert some common typographic texts
smartquotes
: Automatically convert standard quotations to their opening/closing variants
strikethrough
: Enable strikethrough syntax, [see here](syntax/strikethrough) for details
substitution
: Substitute keys, [see here](syntax/substitutions) for details
tasklist
: Add check-boxes to the start of list items, [see here](syntax/tasklists) for details

View File

@ -0,0 +1,4 @@
```{include} ../../CHANGELOG.md
:relative-docs: docs/
:relative-images:
```

View File

@ -0,0 +1,27 @@
# The MyST implementation architecture
This page describes implementation details to help you understand the structure
of the project.
## A Renderer for markdown-it tokens
At a high level, the MyST parser is an extension of th project. Markdown-It-Py
is a well-structured Python parser for CommonMark text. It also defines an extension
point to include more syntax in parsed files. The MyST parser uses this extension
point to define its own syntax options (e.g., for Sphinx roles and directives).
The result of this parser is a markdown-it token stream.
## A docutils renderer
The MyST parser also defines a docutils renderer for the markdown-it token stream.
This allows us to convert parsed elements of a MyST markdown file into docutils.
## A Sphinx parser
Finally, the MyST parser provides a parser for Sphinx, the documentation generation
system. This parser does the following:
* Parse markdown files with the markdown-it parser, including MyST specific plugins
* Convert these files into docutils objects using the MyST docutils renderer
* Provide these to Sphinx in order to use in building your site.

View File

@ -0,0 +1,82 @@
# Background
These sections discuss high-level questions about the MyST ecosystem, and explain a few decisions made in the project.
## Why did we create MyST markdown?
While markdown is ubiquitous, it is not powerful enough for writing modern,
fully-featured documentation. Some flavors of markdown support features needed for this,
but there is no community standard around various syntactic choices for these features.
Sphinx is a documentation generation framework written in Python. It heavily-utilizes
reStructuredText syntax, which is another markup language for writing documents. In
particular, Sphinx defines two extension points that are extremely useful:
**{ref}`in-line roles<sphinx:rst-roles-alt>`** and **{ref}`block-level directives <sphinx:rst-directives>`**.
**This project is an attempt at combining the simplicity and readability of Markdown
with the power and flexibility of reStructuredText and the Sphinx platform.** It
starts with the [CommonMark markdown specification][commonmark], and selectively adds a few extra
syntax pieces to utilize the most powerful parts of reStructuredText.
```{note}
The CommonMark community has been discussing an "official" extension syntax for many
years now (for example, see
[this seven-year-old thread about directives](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444) as well as
[this more recent converstaion](https://talk.commonmark.org/t/support-for-extension-token/2771),
and [this comment listing several more threads on this topic](https://talk.commonmark.org/t/extension-terminology-and-rules/1233)).
We have chosen a "roles and directives" syntax that seems reasonable and follows other
common conventions in Markdown flavors. However, if the CommonMark community ever
decides on an "official" extension syntax, we will likely utilize this syntax for
MyST.
```
## The relationship between MyST, reStructuredText, and Sphinx
MyST markdown provides a markdown equivalent of the reStructuredText syntax,
meaning that you can do anything in MyST that you can do with reStructuredText.
The Sphinx documentation engine supports a number of different input types. By default,
Sphinx reads **reStructuredText** (`.rst`) files. Sphinx uses a **parser** to parse input files
into its own internal document model (which is provided by a core Python project,
[docutils](https://docutils.sourceforge.io/)).
Developers can *extend Sphinx* to support other kinds of input files. Any content file
can be read into the Sphinx document structure, provided that somebody writes a
**parser** for that file. Once a content file has been parsed into Sphinx, it behaves
nearly the same way as any other content file, regardless of the language in which it
was written.
The MyST-parser is a Sphinx parser for the MyST markdown language.
When you use it, Sphinx will know how to parse content files that contain MyST markdown (by default, Sphinx will assume any files ending in `.md` are written in MyST markdown). Once a document has been parsed into Sphinx, it behaves the same way regardless of whether it has been written in rST or MyST markdown.
```
myst markdown (.md) ------> myst parser ---+
|
+-->Sphinx document (docutils)
|
reStructuredText (.rst) --> rst parser ----+
```
For example, here's how you'd write a `toctree` directive in MyST markdown:
````
```{toctree}
My page name <page1>
page2
```
````
and here's the same in rST:
```
.. toctree::
My page name <page1>
page2
```
They will both behave the same in Sphinx.
[commonmark]: https://commonmark.org/

View File

@ -0,0 +1,85 @@
# Contributing
[![Github-CI][github-ci]][github-link]
[![Coverage Status][codecov-badge]][codecov-link]
[![Documentation Status][rtd-badge]][rtd-link]
[![Code style: black][black-badge]][black-link]
We welcome all contributions!
See the [EBP Contributing Guide](https://executablebooks.org/en/latest/contributing.html) for general details, and below for guidance specific to MyST-Parser.
## Install for development
To install `myst-parser` for development, take the following steps:
```bash
git clone https://github.com/executablebooks/MyST-Parser
cd MyST-Parser
git checkout master
pip install -e .[code_style,testing,rtd]
```
## Code Style
Code style is tested using [flake8](http://flake8.pycqa.org),
with the configuration set in `.flake8`,
and code formatted with [black](https://github.com/ambv/black).
Installing with `myst-parser[code_style]` makes the [pre-commit](https://pre-commit.com/)
package available, which will ensure this style is met before commits are submitted, by reformatting the code
and testing for lint errors.
It can be setup by:
```shell
>> cd MyST-Parser
>> pre-commit install
```
Optionally you can run `black` and `flake8` separately:
```shell
>> black .
>> flake8 .
```
Editors like VS Code also have automatic code reformat utilities, which can adhere to this standard.
All functions and class methods should be annotated with types and include a docstring. The preferred docstring format is outlined in `MyST-Parser/docstring.fmt.mustache` and can be used automatically with the
[autodocstring](https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring) VS Code extension.
## Testing
For code tests, myst-parser uses [pytest](https://docs.pytest.org):
```shell
>> cd MyST-Parser
>> pytest
```
You can also use [tox](https://tox.readthedocs.io), to run the tests in multiple isolated environments (see the `tox.ini` file for available test environments):
```shell
>> cd MyST-Parser
>> tox
```
For documentation build tests:
```shell
>> cd MyST-Parser/docs
>> make clean
>> make html-strict
```
```{seealso}
{ref}`develop/testing`
```
[github-ci]: https://github.com/executablebooks/MyST-Parser/workflows/continuous-integration/badge.svg?branch=master
[github-link]: https://github.com/executablebooks/MyST-Parser
[codecov-badge]: https://codecov.io/gh/executablebooks/MyST-Parser/branch/master/graph/badge.svg
[codecov-link]: https://codecov.io/gh/executablebooks/MyST-Parser
[rtd-badge]: https://readthedocs.org/projects/myst-parser/badge/?version=latest
[rtd-link]: https://myst-parser.readthedocs.io/en/latest/?badge=latest
[black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg
[black-link]: https://github.com/ambv/black

15
docs/develop/index.md Normal file
View File

@ -0,0 +1,15 @@
# Contribute
This section covers documentation relevant to developing and maintaining the MyST
codebase, and some guidelines for how you can contribute.
```{toctree}
contributing.md
architecture.md
test_infrastructure.md
```
## Code of Conduct
The MyST-parser project follows the
[Executable Book Project code of conduct](https://github.com/executablebooks/.github/blob/master/CODE_OF_CONDUCT.md).

View File

@ -0,0 +1,53 @@
(develop/testing)=
# Testing Infrastructure
Where possible, additions to the code should be carried out in a
[test-driven development](https://en.wikipedia.org/wiki/Test-driven_development)
manner:
> **Write failing tests that the code should pass, then write code to pass the tests**.
The tests are run using [pytest](https://docs.pytest.org)/[GitHub Actions](https://github.com/features/actions) for unit tests, and [readthedocs](https://readthedocs.org/) for documentation build tests.
The tests are ordered in a hierarchical fashion:
1. In `tests/test_commonmark` the [CommonMark](https://github.com/commonmark/CommonMark.git) test set is run to check that the parser is complying with the CommonMark specification.
2. In `tests/test_renderers` are tests that check that the Markdown AST is being correctly converted to the docutils/sphinx AST. This includes testing that roles and directives are correctly parsed and run.
3. In `tests/test_sphinx` are tests that check that minimal sphinx project builds are running correctly, to convert MyST markdown files to HTML.
4. In `.circleci` the package documentation (written in MyST format) is built and tested for build errors/warnings.
## Test tools
[**pytest-regressions**](https://pytest-regressions.readthedocs.io) is a pytest plugin
that is used in the test suite, to maintain tests that generate lots of data.
In particular, they are used in the syntax testing to generate tests for AST trees
which may change in the future due to changes/additions to the data captured by the parser.
For example, after writing:
```python
def test_example_dict(data_regression):
data_regression.check({
"key1": "value1",
"key2": "value2",
"more": "data...",
})
def test_example_str(file_regression):
file_regression.check("a very long string...")
```
Running the following will initially fail,
but will also generate a file (per test) of expected output:
```console
$ pytest -k test_example
```
Subsequent times the tests are run, the tests output will now be validated against these stored files.
After a change to the syntax parser, all failing tests can then be 'regenerated' with the new
expected output, by running:
```console
$ pytest --force-regen
```

82
docs/docutils.md Normal file
View File

@ -0,0 +1,82 @@
(myst-docutils)=
# Single Page Builds
```{versionadded} 0.16.0
```
Sphinx, and thus MyST-Parser, is built on top of the [Docutils](https://docutils.sourceforge.io/docs/) package.
MyST-Parser offers a renderer, parser and CLI-interface for working directly with Docutils, independent of Sphinx, as described below.
:::{note}
Since these tools are independent of Sphinx, this means they cannot parse any Sphinx or Sphinx extensions specific roles or directives.
:::
On installing MyST-Parser, the following CLI-commands are made available:
- `myst-docutils-html`: converts MyST to HTML
- `myst-docutils-html5`: converts MyST to HTML5
- `myst-docutils-latex`: converts MyST to LaTeX
- `myst-docutils-xml`: converts MyST to docutils-native XML
- `myst-docutils-pseudoxml`: converts MyST to pseudo-XML (to visualise the AST structure)
Each command can be piped stdin or take a file path as an argument:
```console
$ myst-docutils-html --help
$ echo "Hello World" | myst-docutils-html
$ myst-docutils-html hello-world.md
```
The commands are based on the [Docutils Front-End Tools](https://docutils.sourceforge.io/docs/user/tools.html), and so follow the same argument and options structure, included many of the MyST specific options detailed in [](sphinx/config-options).
:::{dropdown} Shared Docutils CLI Options
```{docutils-cli-help}
```
:::
The CLI commands can also utilise the [`docutils.conf` configuration file](https://docutils.sourceforge.io/docs/user/config.html) to configure the behaviour of the CLI commands. For example:
```
# These entries affect all processing:
[general]
myst-enable-extensions: deflist,linkify
myst-footnote-transition: no
# These entries affect specific HTML output:
[html writers]
embed-stylesheet: no
[html5 writer]
stylesheet-dirs: path/to/html5_polyglot/
stylesheet-path: minimal.css, responsive.css
```
You can also use the {py:class}`myst_parser.docutils_.Parser` class programmatically with the [Docutils publisher API](https://docutils.sourceforge.io/docs/api/publisher.html):
```python
from docutils.core import publish_string
from myst_parser.docutils_ import Parser
source = "hallo world\n: Definition"
output = publish_string(
source=source,
writer_name="html5",
settings_overrides={
"myst_enable_extensions": ["deflist"],
"embed_stylesheet": False,
},
parser=Parser(),
)
```
Finally, you can include MyST Markdown files within a RestructuredText file, using the [`include` directive](https://docutils.sourceforge.io/docs/ref/rst/directives.html#include):
```rst
.. include:: include.md
:parser: myst_parser.docutils_
```
```{important}
The `parser` option requires `docutils>=0.17`
```

267
docs/faq/index.md Normal file
View File

@ -0,0 +1,267 @@
(myst-sphinx)=
# FAQ
## How-tos
These sections describe some common scenarios and use-cases for writing MyST with Sphinx.
(howto/include-rst)=
### Include rST files into a Markdown file
As explained in [this section](syntax/directives/parsing), all MyST directives will parse their content as Markdown.
Therefore, using the conventional `include` directive, will parse the file contents as Markdown:
````md
```{include} snippets/include-md.md
```
````
```{include} snippets/include-md.md
```
To include rST, we must first "wrap" the directive in the [eval-rst directive](syntax/directives/parsing):
````md
```{eval-rst}
.. include:: snippets/include-rst.rst
```
````
```{eval-rst}
.. include:: snippets/include-rst.rst
```
(howto/include-md)=
### Include Markdown files into an rST file
To include a MyST file within a ReStructuredText file, we can use the `parser` option of the `include` directive:
```rst
.. include:: include.md
:parser: myst_parser.sphinx_
```
```{important}
The `parser` option requires `docutils>=0.17`
```
### Use MyST in Jupyter Notebooks
The [MyST-NB](https://myst-nb.readthedocs.io) tool provides a Sphinx extension for parsing **Jupyter Notebooks written with MyST Markdown**. It includes features like automatically executing notebooks during documentation builds, storing notebook cell outputs in order to insert them elsewhere in your documentation, and more. See the [MyST-NB documentation](https://myst-nb.readthedocs.io) for more information.
(howto/include-readme)=
### Include a file from outside the docs folder (like README.md)
You can include a file, including one from outside the project using e.g.:
````md
```{include} ../README.md
```
````
**However**, including a file will not usually resolve local links correctly, like `![](my-image.png)`, since it treats the text as if it originated from the "including file".
As of myst-parser version 0.12.7, a new, experimental feature has been added to resolve such links.
You can now use for example:
````md
Source:
```{literalinclude} ../../example.md
:language: md
```
Included:
```{include} ../../example.md
:relative-docs: docs/
:relative-images:
```
````
Source:
```{literalinclude} ../../example-include.md
:language: md
```
Included:
```{include} ../../example-include.md
:relative-docs: docs/
:relative-images:
```
The include here attempts to re-write local links, to reference them from the correct location!
The `relative-docs` must be given the prefix of any links to re-write, to distinguish them from sphinx cross-references.
:::{important}
The current functionality only works for Markdown style images and links.
If you encounter any issues with this feature, please don't hesitate to report it.
:::
(howto/autodoc)=
### Use `sphinx.ext.autodoc` in Markdown files
The [Sphinx extension `autodoc`](sphinx:sphinx.ext.autodoc), which pulls in code documentation from docstrings, is currently hard-coded to parse reStructuredText.
It is therefore incompatible with MyST's Markdown parser.
However, the special [`eval-rst` directive](syntax/directives/parsing) can be used to "wrap" `autodoc` directives:
````md
```{eval-rst}
.. autoclass:: myst_parser.mocking.MockRSTParser
:show-inheritance:
:members: parse
```
````
```{eval-rst}
.. autoclass:: myst_parser.mocking.MockRSTParser
:show-inheritance:
:members: parse
```
As with other objects in MyST, this can then be referenced:
- Using the role `` {py:class}`myst_parser.mocking.MockRSTParser` ``: {py:class}`myst_parser.mocking.MockRSTParser`
- Using the Markdown syntax `[MockRSTParser](myst_parser.mocking.MockRSTParser)`: [MockRSTParser](myst_parser.mocking.MockRSTParser)
```{warning}
This expects docstrings to be written in reStructuredText.
We hope to support Markdown in the future, see [GitHub issue #228](https://github.com/executablebooks/MyST-Parser/issues/228).
```
(howto/autosectionlabel)=
### Automatically create targets for section headers
:::{important}
New in `v0.13.0` ✨, myst-parser now provides a separate implementation of `autosectionlabel`, which implements GitHub Markdown style bookmark anchors, like `[](file.md#header-anchor)`.
See the [](syntax/header-anchors) section of extended syntaxes.
:::
If you'd like to *automatically* generate targets for each of your section headers,
check out the [`autosectionlabel`](https://www.sphinx-doc.org/en/master/usage/extensions/autosectionlabel.html)
sphinx feature. You can activate it in your Sphinx site by adding the following to your
`conf.py` file:
```python
extensions = [
'sphinx.ext.autosectionlabel',
]
# Prefix document path to section labels, to use:
# `path/to/file:heading` instead of just `heading`
autosectionlabel_prefix_document = True
```
So, if you have a page at `myfolder/mypage.md` (relative to your documentation root)
with the following structure:
```md
# Title
## My Subtitle
```
Then the `autosectionlabel` feature will allow you to reference the section headers
like so:
```md
{ref}`path/to/file_1:My Subtitle`
```
(howto/warnings)=
### Suppress warnings
In general, if your build logs any warnings, you should either fix them or [raise an Issue](https://github.com/executablebooks/MyST-Parser/issues/new/choose) if you think the warning is erroneous.
However, in some circumstances if you wish to suppress the warning you can use the [`suppress_warnings`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-suppress_warnings) configuration option.
All myst-parser warnings are prepended by their type, e.g. to suppress:
```md
# Title
### Subtitle
```
```
WARNING: Non-consecutive header level increase; H1 to H3 [myst.header]
```
Add to your `conf.py`:
```python
suppress_warnings = ["myst.header"]
```
### Sphinx-specific page front matter
Sphinx intercepts front matter and stores them within the global environment
(as discussed [in the deflists documentation](https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html)).
There are certain front-matter keys (or their translations) that are also recognised specifically by docutils and parsed to inline Markdown:
- `author`
- `authors`
- `organization`
- `address`
- `contact`
- `version`
- `revision`
- `status`
- `date`
- `copyright`
- `dedication`
- `abstract`
A classic use-case is to specify 'orphan' documents, that are not specified in any toctrees.
For example, inserting the following syntax at the top of a page will cause Sphinx to treat it as an orphan page:
```md
---
orphan: true
---
This is an orphan document, not specified in any toctrees.
```
### Migrate pre-existing rST into MyST
If you've already got some reStructuredText files that you'd like to convert into MyST Markdown, try the [`rst-to-myst`](https://github.com/executablebooks/rst-to-myst) tool, which allows you to convert single rST files to MyST markdown documents.
## Disable Markdown syntax for the parser
If you'd like to either enable or disable custom markdown syntax, use `myst_disable_syntax`.
Anything in this list will no longer be parsed by the MyST parser.
For example, to disable the `emphasis` in-line syntax, use this configuration:
```python
myst_disable_syntax = ["emphasis"]
```
emphasis syntax will now be disabled. For example, the following will be rendered
*without* any italics:
```md
*emphasis is now disabled*
```
For a list of all the syntax elements you can disable, see the [markdown-it parser guide](markdown_it:using).
## Common errors and questions
These are common issues and gotchas that people may experience when using the MyST Sphinx extension.
### What markup language should I use inside directives?
If you need to parse content *inside* of another block of content (for example, the
content inside a **note directive**), note that the MyST parser will be used for this
nested parsing as well.
### Why doesn't my role/directive recognize markdown link syntax?
There are some roles/directives that _hard-code_ syntax into
their behavior. For example, many roles allow you to supply titles for links like so:
`` {role}`My title <myref>` ``. While this looks like reStructuredText, the role may
be explicitly expecting the `My title <myref>` structure, and so MyST will behave the same way.

View File

@ -0,0 +1 @@
Hallo I'm from a Markdown file, [with a reference](howto/autodoc).

View File

@ -0,0 +1 @@
Hallo I'm from an rST file, :ref:`with a reference <howto/autodoc>`.

160
docs/index.md Normal file
View File

@ -0,0 +1,160 @@
---
sd_hide_title: true
---
# Overview
::::{grid}
:reverse:
:gutter: 3 4 4 4
:margin: 1 2 1 2
:::{grid-item}
:columns: 12 4 4 4
```{image} _static/logo-square.svg
:width: 200px
:class: sd-m-auto
```
:::
:::{grid-item}
:columns: 12 8 8 8
:child-align: justify
:class: sd-fs-5
```{rubric} MyST - Markedly Structured Text - Parser
```
A Sphinx and Docutils extension to parse MyST,
a rich and extensible flavour of Markdown for authoring technical and scientific documentation.
```{button-ref} intro
:ref-type: doc
:color: primary
:class: sd-rounded-pill
Get Started
```
:::
::::
---
::::{grid} 1 2 2 3
:gutter: 1 1 1 2
:::{grid-item-card} {octicon}`markdown;1.5em;sd-mr-1` CommonMark-plus
:link: syntax/core
:link-type: ref
MyST extends the CommonMark syntax specification, to support technical authoring features such as tables and footnotes.
+++
[Learn more »](syntax/core)
:::
:::{grid-item-card} {octicon}`plug;1.5em;sd-mr-1` Sphinx compatible
:link: roles-directives
:link-type: ref
Use the MyST role and directive syntax to harness the full capability of Sphinx, such as admonitions and figures, and all existing Sphinx extensions.
+++
[Learn more »](roles-directives)
:::
:::{grid-item-card} {octicon}`tools;1.5em;sd-mr-1` Highly configurable
:link: configuration
:link-type: doc
MyST-parser can be configured at both the global and individual document level,
to modify parsing behaviour and access extended syntax features.
+++
[Learn more »](configuration)
:::
::::
---
```{rubric} Additional resources
```
[MyST-Markdown VS Code extension](https://marketplace.visualstudio.com/items?itemName=ExecutableBookProject.myst-highlight)
: For MyST extended syntax highlighting and authoring tools.
[Convert existing ReStructuredText files to Markdown][rst-to-myst]
: Use the [rst-to-myst] CLI or [the MySTyc interactive web interface](https://astrojuanlu.github.io/mystyc/).
[MyST-NB](https://myst-nb.readthedocs.io)
: A Sphinx and Docutils extension for compiling Jupyter Notebooks into high quality documentation formats, built on top of the MyST-Parser.
[Jupyter Book](https://jupyterbook.org)
: An open source project for building beautiful, publication-quality books and documents from computational material, built on top of the MyST-Parser and MyST-NB.
[The Jupyter Book gallery](https://executablebooks.org/en/latest/gallery.html)
: Examples of documents built with MyST.
[Javascript MyST parser][mystjs]
: The [mystjs] Javascript parser, allows you to parse MyST in websites.
[markdown-it-py]
: A CommonMark-compliant and extensible Markdown parser, used by MyST-Parser to parse source text to tokens.
```{rubric} Acknowledgements
```
The MyST markdown language and MyST parser are both supported by the open community,
[The Executable Book Project](https://executablebooks.org).
```{toctree}
:hidden:
intro.md
```
```{toctree}
:hidden:
:caption: Guides
syntax/syntax
syntax/optional
syntax/roles-and-directives.md
configuration.md
docutils.md
faq/index.md
develop/index.md
```
```{toctree}
:hidden:
:caption: Reference
develop/_changelog.md
syntax/reference
develop/background.md
api/reference.rst
```
[commonmark]: https://commonmark.org/
[github-ci]: https://github.com/executablebooks/MyST-Parser/workflows/continuous-integration/badge.svg?branch=master
[github-link]: https://github.com/executablebooks/MyST-Parser
[codecov-badge]: https://codecov.io/gh/executablebooks/MyST-Parser/branch/master/graph/badge.svg
[codecov-link]: https://codecov.io/gh/executablebooks/MyST-Parser
[rtd-badge]: https://readthedocs.org/projects/myst-parser/badge/?version=latest
[rtd-link]: https://myst-parser.readthedocs.io/en/latest/?badge=latest
[black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg
[pypi-badge]: https://img.shields.io/pypi/v/myst-parser.svg
[pypi-link]: https://pypi.org/project/myst-parser
[conda-badge]: https://anaconda.org/conda-forge/myst-parser/badges/version.svg
[conda-link]: https://anaconda.org/conda-forge/myst-parser
[black-link]: https://github.com/ambv/black
[github-badge]: https://img.shields.io/github/stars/executablebooks/myst-parser?label=github
[markdown-it-py]: https://markdown-it-py.readthedocs.io/
[markdown-it]: https://markdown-it.github.io/
[rst-to-myst]: https://rst-to-myst.readthedocs.io
[mystjs]: https://github.com/executablebooks/mystjs

250
docs/intro.md Normal file
View File

@ -0,0 +1,250 @@
(intro/get-started)=
# Get Started
This page describes how to get started with the MyST parser, with a focus on enabling it in the Sphinx documentation engine.
## Installation
[![PyPI][pypi-badge]][pypi-link]
[![Conda][conda-badge]][conda-link]
To install use [pip](https://pip.pypa.io):
```bash
pip install myst-parser
```
or [Conda](https://docs.conda.io):
```bash
conda install -c conda-forge myst-parser
```
[pypi-badge]: https://img.shields.io/pypi/v/myst-parser.svg
[pypi-link]: https://pypi.org/project/myst-parser
[conda-badge]: https://anaconda.org/conda-forge/myst-parser/badges/version.svg
[conda-link]: https://anaconda.org/conda-forge/myst-parser
(intro/sphinx)=
## Enable MyST in Sphinx
To get started with Sphinx, see their [Quickstart Guide](https://www.sphinx-doc.org/en/master/usage/quickstart.html).
To use the MyST parser in Sphinx, simply add the following to your `conf.py` file:
```python
extensions = ["myst_parser"]
```
This will activate the MyST Parser extension, causing all documents with the `.md` extension to be parsed as MyST.
:::{tip}
To parse single documents, see the [](docutils.md) section
:::
(intro/writing)=
## Write a CommonMark document
MyST is an extension of [CommonMark Markdown](https://commonmark.org/),
that includes [additional syntax](../syntax/syntax.md) for technical authoring,
which integrates with Docutils and Sphinx.
To start off, create an empty file called `myfile.md` and give it a markdown title and text.
```md
# My nifty title
Some **text**!
```
To parse to HTML, try the CLI:
```html
$ myst-docutils-html5 --stylesheet= myfile.md
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>My nifty title</title>
</head>
<body>
<main id="my-nifty-title">
<h1 class="title">My nifty title</h1>
<p>Some <strong>text</strong>!</p>
</main>
</body>
</html>
```
To include this document within a Sphinx project,
include `myfile.md` in a [`toctree` directive](sphinx:toctree-directive) on an index page.
## Extend CommonMark with roles and directives
MyST allows any Sphinx role or directive to be used in a document.
These are extensions points allowing for richer features, such as admonitions and figures.
For example, add an `admonition` directive and `sup` role to your Markdown page, like so:
````md
# My nifty title
Some **text**!
```{admonition} Here's my title
:class: tip
Here's my admonition content.{sup}`1`
```
````
Then convert to HTML:
```html
$ myst-docutils-html5 --stylesheet= myfile.md
...
<div class="admonition tip">
<p class="admonition-title">Here's my title</p>
<p>Here's my admonition content.<sup>1</sup></p>
</div>
...
```
:::{seealso}
The full [](syntax/roles-and-directives.md) section
:::
(intro/reference)=
## Cross-referencing
MyST-Parser offers powerful cross-referencing features, to link to documents, headers, figures and more.
For example, to add a section *reference target*, and reference it:
```md
(header-label)=
# A header
[My reference](header-label)
```
```html
$ myst-docutils-html5 --stylesheet= myfile.md
...
<span id="header-label"></span>
<h1 class="title">A header</h1>
<p><a class="reference internal" href="#header-label">My reference</a></p>
...
```
:::{seealso}
The [](syntax/referencing) section,\
and the [ReadTheDocs cross-referencing](https://docs.readthedocs.io/en/stable/guides/cross-referencing-with-sphinx.html) documentation
:::
## Configuring MyST-Parser
The [](configuration.md) section contains a complete list of configuration options for the MyST-Parser.
These can be applied globally, e.g. in the sphinx `conf.py`:
```python
myst_enable_extensions = [
"colon_fence",
]
```
Or they can be applied to specific documents, at the top of the document:
```yaml
---
myst:
enable_extensions: ["colon_fence"]
---
```
## Extending Sphinx
The other way to extend MyST in Sphinx is to install Sphinx extensions that define new roles, directives, etc.
For example, let's install the `sphinxcontrib.mermaid` extension,
which will allow us to generate [Mermaid diagrams](https://mermaid-js.github.io/mermaid/#/) with MyST.
First, install `sphinxcontrib.mermaid`:
```shell
pip install sphinxcontrib-mermaid
```
Next, add it to your list of extensions in `conf.py`:
```python
extensions = [
"myst_parser",
"sphinxcontrib.mermaid",
]
```
Now, add a **mermaid directive** to your markdown file.
For example:
````md
# My nifty title
Some **text**!
```{admonition} Here's my title
:class: warning
Here's my admonition content
```
(section-two)=
## Here's another section
And some more content.
% This comment won't make it into the outputs!
And here's {ref}`a reference to this section <section-two>`.
I can also reference the section {ref}`section-two` without specifying my title.
:::{note}
And here's a note with a colon fence!
:::
And finally, here's a cool mermaid diagram!
```{mermaid}
sequenceDiagram
participant Alice
participant Bob
Alice->John: Hello John, how are you?
loop Healthcheck
John->John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail...
John-->Alice: Great!
John->Bob: How about you?
Bob-->John: Jolly good!
```
````
When you build your documentation, you should see something like this:
```{mermaid}
sequenceDiagram
participant Alice
participant Bob
Alice->John: Hello John, how are you?
loop Healthcheck
John->John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail...
John-->Alice: Great!
John->Bob: How about you?
Bob-->John: Jolly good!
```

1
docs/syntax/example.txt Normal file
View File

@ -0,0 +1 @@
Hallo!

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

1002
docs/syntax/optional.md Normal file

File diff suppressed because it is too large Load Diff

261
docs/syntax/reference.md Normal file
View File

@ -0,0 +1,261 @@
(syntax-tokens)=
# Syntax tokens
This page serves as a reference for the syntax that makes of MyST Markdown.
:::{seealso}
For more description and explanation of MyST syntax, see the [syntax guide](syntax.md).
:::
## Block (Multi-line) Tokens
Block tokens span multiple lines of content. They are broken down into two sections:
- {ref}`extended-block-tokens` contains *extra* tokens that are not in CommonMark.
- {ref}`commonmark-block-tokens` contains CommonMark tokens that also work, for reference.
:::{note}
Because MyST markdown was inspired by functionality that exists in reStructuredText,
we have shown equivalent rST syntax for many MyST markdown features below.
:::
(extended-block-tokens)=
### Extended block tokens
`````{list-table}
:header-rows: 1
:widths: 10 20 20
* - Token
- Description
- Example
* - FrontMatter
- A YAML block at the start of the document enclosed by `---`
- ```yaml
---
key: value
---
```
* - Directives
- enclosed in 3 or more backticks followed by the directive name wrapped
in curly brackets `{}`. See {ref}`syntax/directives` for more details.
- ````md
```{directive}
:option: value
content
```
````
* - Math
- `$$` (default) or `\[`...`\]` characters wrapping multi-line math, or even direct [amsmath](https://ctan.org/pkg/amsmath) LaTeX equations (optional).
See {ref}`syntax/math` for more information.
- ```latex
$$
a=1
$$
```
* - Table
- Standard markdown table style, with pipe separation.
- ```md
| a | b |
| :--- | ---: |
| c | d |
```
* - LineComment
- A commented line. See {ref}`syntax/comments` for more information.
- ```latex
% this is a comment
```
* - BlockBreak
- Define blocks of text. See {ref}`syntax/blockbreaks` for more information.
- ```md
+++ {"meta": "data"}
```
* - Footnote
- A definition for a referencing footnote, that is placed at the bottom of the document.
See {ref}`syntax/footnotes` for more details.
- ```md
[^ref]: Some footnote text
```
* - Admonitions (optional)
- An alternative approach for admonition style directives only, which has the benefit of allowing the content to be rendered in standard markdown editors.
See [admonition directives](syntax/admonitions) for more details.
- ````md
:::{note}
*content*
:::
````
`````
(commonmark-block-tokens)=
### CommonMark tokens
`````{list-table}
:header-rows: 1
:widths: 10 20 20
* - Token
- Description
- Example
* - HTMLBlock
- Any valid HTML (rendered in HTML output only)
- ```html
<p>some text</p>
```
* - BlockCode
- indented text (4 spaces or a tab)
- ```md
included as literal *text*
```
* - Heading
- Level 1-6 headings, denoted by number of `#`
- ```md
### Heading level 3
```
* - SetextHeading
- Underlined header (using multiple `=` or `-`)
- ```md
Header
======
```
* - Quote
- Quoted text
- ```md
> this is a quote
```
* - CodeFence
- Enclosed in 3 or more `` ` `` or `~` with an optional language name.
See {ref}`syntax/code-blocks` for more information.
- ````md
```python
print('this is python')
```
````
* - ThematicBreak
- Creates a horizontal line in the output
- ```md
---
```
* - List
- bullet points or enumerated.
- ```md
- item
- nested item
1. numbered item
```
* - LinkDefinition
- A substitution for an inline link, which can have a reference target (no spaces), and an optional title (in `"`)
- ```md
[key]: https://www.google.com "a title"
```
* - Paragraph
- General inline text
- ```md
any *text*
```
`````
## Span (Inline) Tokens
Span (or inline) tokens are defined on a single line of content. They are broken down into two
sections below:
- {ref}`extended-span-tokens` contains *extra* tokens that are not in CommonMark.
- {ref}`commonmark-span-tokens` contains CommonMark tokens that also work, for reference.
(extended-span-tokens)=
### Extended inline tokens
`````{list-table}
:header-rows: 1
:widths: 10 20 20
* - Token
- Description
- Example
* - Role
- See {ref}`syntax/roles` for more information.
- ```md
{rolename}`interpreted text`
```
* - Target
- Precedes element to target, e.g. header. See
{ref}`syntax/targets` for more information.
- ```md
(target)=
```
* - Math
- `$` (default) or `\(`...`\)` enclosed math. See
{ref}`syntax/math` for more information.
- ```latex
$a=1$ or $$a=1$$
```
* - FootReference
- Reference a footnote. See {ref}`syntax/footnotes` for more details.
- ```md
[^abc]
```
`````
(commonmark-span-tokens)=
### CommonMark inline tokens
`````{list-table}
:header-rows: 1
:widths: 10 20 20
* - Token
- Description
- Example
* - HTMLSpan
- Any valid HTML (rendered in HTML output only)
- ```html
<p>some text</p>
```
* - EscapeSequence
- Escaped symbols (to avoid them being interpreted as other syntax elements)
- ```md
\*
```
* - AutoLink
- Link that is shown in final output
- ```md
<http://www.google.com>
```
* - InlineCode
- Literal text
- ```md
`a=1`
```
* - LineBreak
- Soft or hard (ends with spaces or backslash)
- ```md
A hard break\
```
* - Image
- Link to an image.
You can also use HTML syntax, to include image size etc, [see here](syntax/images) for details
- ```md
![alt](src "title")
```
* - Link
- Reference `LinkDefinitions`. See {ref}`syntax/referencing` for more details.
- ```md
[text](target "title") or [text][key]
```
* - Strong
- Bold text
- ```md
**strong**
```
* - Emphasis
- Italic text
- ```md
*emphasis*
```
* - RawText
- Any text
- ```md
any text
```
`````

View File

@ -0,0 +1,421 @@
(roles-directives)=
# Roles and Directives
Roles and directives provide a way to extend the syntax of MyST in an unbound manner,
by interpreting a chuck of text as a specific type of markup, according to its name.
Mostly all [docutils roles](https://docutils.sourceforge.io/docs/ref/rst/roles.html), [docutils directives](https://docutils.sourceforge.io/docs/ref/rst/directives.html), [sphinx roles](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html), or [sphinx directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html) can be used in MyST.
## Syntax
(syntax/directives)=
### Directives - a block-level extension point
Directives syntax is defined with triple-backticks and curly-brackets.
It is effectively a Markdown code fence with curly brackets around the language, and a directive name in place of a language name.
Here is the basic structure:
`````{list-table}
---
header-rows: 1
---
* - MyST
- reStructuredText
* - ````md
```{directivename} arguments
---
key1: val1
key2: val2
---
This is
directive content
```
````
- ```rst
.. directivename:: arguments
:key1: val1
:key2: val2
This is
directive content
```
`````
For example, the following code:
````md
```{admonition} This is my admonition
This is my note
```
````
Will generate this admonition:
```{admonition} This is my admonition
This is my note
```
#### Parameterizing directives
For directives that take parameters as input, there are two ways to parameterize them.
In each case, the options themselves are given as `key: value` pairs. An example of
each is shown below:
**Using YAML frontmatter**. A block of YAML front-matter just after the
first line of the directive will be parsed as options for the directive. This needs to be
surrounded by `---` lines. Everything in between will be parsed by YAML and
passed as keyword arguments to your directive. For example:
````md
```{code-block} python
---
lineno-start: 10
emphasize-lines: 1, 3
caption: |
This is my
multi-line caption. It is *pretty nifty* ;-)
---
a = 2
print('my 1st line')
print(f'my {a}nd line')
```
````
```{code-block} python
---
lineno-start: 10
emphasize-lines: 1, 3
caption: |
This is my
multi-line caption. It is *pretty nifty* ;-)
---
a = 2
print('my 1st line')
print(f'my {a}nd line')
```
**Short-hand options with `:` characters**. If you only need one or two options for your
directive and wish to save lines, you may also specify directive options as a collection
of lines just after the first line of the directive, each preceding with `:`. Then the
leading `:` is removed from each line, and the rest is parsed as YAML.
For example:
````md
```{code-block} python
:lineno-start: 10
:emphasize-lines: 1, 3
a = 2
print('my 1st line')
print(f'my {a}nd line')
```
````
(syntax/directives/parsing)=
#### How directives parse content
Some directives parse the content that is in their content block.
MyST parses this content **as Markdown**.
This means that MyST markdown can be written in the content areas of any directives written in MyST markdown. For example:
````md
```{admonition} My markdown link
Here is [markdown link syntax](https://jupyter.org)
```
````
```{admonition} My markdown link
Here is [markdown link syntax](https://jupyter.org)
```
As a short-hand for directives that require no arguments, and when no parameter options are used (see below),
you may start the content directly after the directive name.
````md
```{note} Notes require **no** arguments, so content can start here.
```
````
```{note} Notes require **no** arguments, so content can start here.
```
For special cases, MySt also offers the `eval-rst` directive.
This will parse the content **as ReStructuredText**:
````md
```{eval-rst}
.. figure:: img/fun-fish.png
:width: 100px
:name: rst-fun-fish
Party time!
A reference from inside: :ref:`rst-fun-fish`
A reference from outside: :ref:`syntax/directives/parsing`
```
````
```{eval-rst}
.. figure:: img/fun-fish.png
:width: 100px
:name: rst-fun-fish
Party time!
A reference from inside: :ref:`rst-fun-fish`
A reference from outside: :ref:`syntax/directives/parsing`
```
Note how the text is integrated into the rest of the document, so we can also reference [party fish](rst-fun-fish) anywhere else in the documentation.
#### Nesting directives
You can nest directives by ensuring that the tick-lines corresponding to the
outermost directive are longer than the tick-lines for the inner directives.
For example, nest a warning inside a note block like so:
`````md
````{note}
The next info should be nested
```{warning}
Here's my warning
```
````
`````
Here's how it looks rendered:
````{note}
The next info should be nested
```{warning}
Here's my warning
```
````
You can indent inner-code fences, so long as they aren't indented by more than 3 spaces.
Otherwise, they will be rendered as "raw code" blocks:
`````md
````{note}
The warning block will be properly-parsed
```{warning}
Here's my warning
```
But the next block will be parsed as raw text
```{warning}
Here's my raw text warning that isn't parsed...
```
````
`````
````{note}
The warning block will be properly-parsed
```{warning}
Here's my warning
```
But the next block will be parsed as raw text
```{warning}
Here's my raw text warning that isn't parsed...
```
````
This can really be abused if you'd like ;-)
``````{note}
The next info should be nested
`````{warning}
Here's my warning
````{admonition} Yep another admonition
```python
# All this fuss was about this boring python?!
print('yep!')
```
````
`````
``````
#### Markdown-friendly directives
Want to use syntax that renders correctly in standard Markdown editors?
See [the extended syntax option](syntax/colon_fence).
```md
:::{note}
This text is **standard** *Markdown*
:::
```
:::{note}
This text is **standard** *Markdown*
:::
(syntax/roles)=
### Roles - an in-line extension point
Roles are similar to directives - they allow you to define arbitrary new functionality, but they are used *in-line*.
To define an in-line role, use the following form:
````{list-table}
---
header-rows: 1
---
* - MyST
- reStructuredText
* - ````md
{role-name}`role content`
````
- ```rst
:role-name:`role content`
```
````
For example, the following code:
```md
Since Pythagoras, we know that {math}`a^2 + b^2 = c^2`
```
Becomes:
Since Pythagoras, we know that {math}`a^2 + b^2 = c^2`
You can use roles to do things like reference equations and other items in
your book. For example:
````md
```{math} e^{i\pi} + 1 = 0
---
label: euler
---
```
Euler's identity, equation {math:numref}`euler`, was elected one of the
most beautiful mathematical formulas.
````
Becomes:
```{math} e^{i\pi} + 1 = 0
---
label: euler
---
```
Euler's identity, equation {math:numref}`euler`, was elected one of the
most beautiful mathematical formulas.
#### How roles parse content
The content of roles is parsed differently depending on the role that you've used.
Some roles expect inputs that will be used to change functionality. For example,
the `ref` role will assume that input content is a reference to some other part of the
site. However, other roles may use the MyST parser to parse the input as content.
Some roles also **extend their functionality** depending on the content that you pass.
For example, following the `ref` example above, if you pass a string like this:
`Content to display <myref>`, then the `ref` will display `Content to display` and use
`myref` as the reference to look up.
How roles parse this content depends on the author that created the role.
## Common roles and directives
:::{admonition} {material-regular}`engineering;1.5rem;sd-mr-1` Currently Under Construction
:class: no-icon
Check back for more...
:::
### ToC Trees
```{doc-directive} contents
Insert a table of contents tree of the documents headings.
```
```{doc-directive} toctree
Inserts a Sphinx "Table of Contents" tree, containing a list of (relative) child document paths.
```
### Admonitions
```{doc-directive} admonition
Create a generic "callout" box, containing the content.
```
```{doc-directive} note
Create a "callout" box, specific to notes, containing the content.
```
Other admonitions (same structure as `note`): `attention`, `caution`, `danger`, `error`, `hint`, `important`, `tip`, `warning`.
Sphinx only: `deprecated`, `versionadded`, `versionchanged`.
### Images and Figures
```{doc-directive} image
Insert an image, from a (relative) path or URL.
```
```{doc-directive} figure
Insert an image, from a (relative) path or URL,
with a caption (first paragraph), and optional legend (subsequent content).
```
```{doc-directive} table
Insert a (MyST) table with a caption.
```
### Tables
```{doc-directive} list-table
Create a table from data in a uniform two-level bullet list.
```
```{doc-directive} csv-table
Create a table from CSV (comma-separated values) data.
```
### Code
```{doc-directive} code-block
Syntax highlight a block of code, according to the language.
```
(syntax/roles/special)=
### MyST only
This section contains information about special roles and directives that come bundled with the MyST Parser Sphinx extension.
#### Insert the date and reading time
```{versionadded} 0.14.0
The `sub-ref` role and word counting.
```
You may insert the "last updated" date and estimated reading time into your document via substitution definitions, which can be accessed *via* the `sub-ref` role.
For example:
```markdown
> {sub-ref}`today` | {sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
```
> {sub-ref}`today` | {sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
`today` is replaced by either the date on which the document is parsed, with the format set by [`today_fmt`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-today_fmt), or the `today` variable if set in the configuration file.
The reading speed is computed using the `myst_words_per_minute` configuration (see the [Sphinx configuration options](sphinx/config-options)).

491
docs/syntax/syntax.md Normal file
View File

@ -0,0 +1,491 @@
(syntax/core)=
# Core Syntax
## Introduction
MyST is a strict superset of the [CommonMark syntax specification](https://spec.commonmark.org/).
It adds features focussed on scientific and technical documentation authoring, as detailed below.
In addition, the roles and directives syntax provide inline/block-level extension points for plugins.
This is detailed further in the [Roles and Directives](roles-directives) section.
:::{seealso}
The [syntax token reference tables](syntax-tokens)
:::
(syntax/commonmark)=
## CommonMark
The [CommonMark syntax specification](https://spec.commonmark.org/) details the full set of syntax rules.
Here we provide a summary of most features:
Element | Syntax
--------------- | -------------------------------------------
Heading | `# H1` to `###### H6`
Bold | `**bold**`
Italic | `*italic*`
Inline Code | `` `code` ``
Autolink | `<https://www.example.com>`
URL Link | `[title](https://www.example.com)`
Image | `![alt](https://www.example.com/image.png)`
Reference Link | `[title][link]`
Link Definition | `[link]: https://www.example.com`
Thematic break | `---`
Blockquote | `> quote`
Ordered List | `1. item`
Unordered List | `- item`
Code Fence | opening ```` ```lang ```` to closing ```` ``` ````
(syntax/frontmatter)=
## Front Matter
This is a [YAML](https://en.wikipedia.org/wiki/YAML) block at the start of the document, as used for example in [jekyll](https://jekyllrb.com/docs/front-matter/).
The document should start with three or more `---` markers, and YAML is parsed until a closing `---` marker is found:
```yaml
---
key1: value
key2: [value1, value2]
key3:
subkey1: value
---
```
:::{seealso}
Top-matter is also used for the [substitution syntax extension](syntax/substitutions),
and can be used to store information for blog posting (see [ablog's myst-parser support](https://ablog.readthedocs.io/en/latest/manual/markdown/)).
:::
### Setting a title
```{versionadded} 0.17.0
```
If `myst_title_to_header` is set to `True`, and a `title` key is present in the front matter,
then the title will be used as the document's header (parsed as Markdown).
For example:
```md
---
title: My Title with *emphasis*
---
```
would be equivalent to:
```md
# My Title with *emphasis*
```
(syntax/html_meta)=
### Setting HTML Metadata
The front-matter can contain the special key `html_meta`; a dict with data to add to the generated HTML as [`<meta>` elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta).
This is equivalent to using the [RST `meta` directive](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#html-metadata).
HTML metadata can also be added globally in the `conf.py` *via* the `myst_html_meta` variable, in which case it will be added to all MyST documents.
For each document, the `myst_html_meta` dict will be updated by the document level front-matter `html_meta`, with the front-matter taking precedence.
::::{tab-set}
:::{tab-item} Sphinx Configuration
```python
language = "en"
myst_html_meta = {
"description lang=en": "metadata description",
"description lang=fr": "description des métadonnées",
"keywords": "Sphinx, MyST",
"property=og:locale": "en_US"
}
```
:::
:::{tab-item} MyST Front-Matter
```yaml
---
myst:
html_meta:
"description lang=en": "metadata description"
"description lang=fr": "description des métadonnées"
"keywords": "Sphinx, MyST"
"property=og:locale": "en_US"
---
```
:::
:::{tab-item} RestructuredText
```restructuredtext
.. meta::
:description lang=en: metadata description
:description lang=fr: description des métadonnées
:keywords: Sphinx, MyST
:property=og:locale: en_US
```
:::
:::{tab-item} HTML Output
```html
<html lang="en">
<head>
<meta content="metadata description" lang="en" name="description" xml:lang="en" />
<meta content="description des métadonnées" lang="fr" name="description" xml:lang="fr" />
<meta name="keywords" content="Sphinx, MyST">
<meta content="en_US" property="og:locale" />
```
:::
::::
(syntax/comments)=
## Comments
You may add comments by putting the `%` character at the beginning of a line. This will
prevent the line from being parsed into the output document.
For example, this code:
```md
% my comment
```
Is below, but it won't be parsed into the document.
% my comment
````{important}
Since comments are a block-level entity, they will terminate the previous block.
In practical terms, this means that the following lines
will be broken up into two paragraphs, resulting in a new line between them:
```
a line
% a comment
another line
```
a line
% a comment
another line
````
:::{tip}
Comments are equivalent to the RST syntax: `.. my comment`.
:::
(syntax/blockbreaks)=
## Block Breaks
You may add a block break by putting `+++` at the beginning of a line.
This constuct's intended use case is for mapping to cell based document formats,
like [jupyter notebooks](https://jupyter.org/),
to indicate a new text cell. It will not show up in the rendered text,
but is stored in the internal document structure for use by developers.
For example, this code:
```md
+++ some text
```
Is below, but it won't be parsed into the document.
+++
(syntax/referencing)=
## Markdown Links and Referencing
Markdown links are of the form: `[text](link)`.
If you set the configuration `myst_all_links_external = True` (`False` by default),
then all links will be treated simply as "external" links.
For example, in HTML outputs, `[text](link)` will be rendered as `<a href="link">text</a>`.
Otherwise, links will only be treated as "external" links if they are prefixed with a scheme,
configured with `myst_url_schemes` (by default, `http`, `https`, `ftp`, or `mailto`).
For example, `[example.com](https://example.com)` becomes [example.com](https://example.com).
:::{note}
The `text` will be parsed as nested Markdown, for example `[here's some *emphasised text*](https://example.com)` will be parsed as [here's some *emphasised text*](https://example.com).
:::
For "internal" links, myst-parser in Sphinx will attempt to resolve the reference to either a relative document path, or a cross-reference to a target (see [](syntax/targets)):
- `[this doc](syntax.md)` will link to a rendered source document: [this doc](syntax.md)
- This is similar to `` {doc}`this doc <syntax>` ``; {doc}`this doc <syntax>`, but allows for document extensions, and parses nested Markdown text.
- `[example text](example.txt)` will link to a non-source (downloadable) file: [example text](example.txt)
- The linked document itself will be copied to the build directory.
- This is similar to `` {download}`example text <example.txt>` ``; {download}`example text <example.txt>`, but parses nested Markdown text.
- `[reference](syntax/referencing)` will link to an internal cross-reference: [reference](syntax/referencing)
- This is similar to `` {any}`reference <syntax/referencing>` ``; {any}`reference <syntax/referencing>`, but parses nested Markdown text.
- You can limit the scope of the cross-reference to specific [sphinx domains](sphinx:domain), by using the `myst_ref_domains` configuration.
For example, `myst_ref_domains = ("std", "py")` will only allow cross-references to `std` and `py` domains.
Additionally, only if [](syntax/header-anchors) are enabled, then internal links to document headers can be used.
For example `[a header](syntax.md#markdown-links-and-referencing)` will link to a header anchor: [a header](syntax.md#markdown-links-and-referencing).
(syntax/targets)=
## Targets and Cross-Referencing
Targets are used to define custom anchors that you can refer to elsewhere in your
documentation. They generally go before section titles so that you can easily refer
to them.
:::{tip}
If you'd like to *automatically* generate targets for each of your section headers,
check out the [](syntax/header-anchors) section of extended syntaxes.
:::
Target headers are defined with this syntax:
```md
(header_target)=
```
They can then be referred to with the [ref inline role](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref):
```md
{ref}`header_target`
```
By default, the reference will use the text of the target (such as the section title), but also you can directly specify the text:
```md
{ref}`my text <header_target>`
```
For example, see this ref: {ref}`syntax/targets`, and here's a ref back to the top of this page: {ref}`my text <syntax/core>`.
Alternatively using the markdown syntax:
```md
[my text](header_target)
```
is equivalent to using the [any inline role](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-any):
```md
{any}`my text <header_target>`
```
but can also accept "nested" syntax (like bold text) and will recognise document paths that include extensions (e.g. `syntax/syntax` or `syntax/syntax.md`)
Using the same example, see this ref: [](syntax/targets), here is a reference back to the top of
this page: [my text with **nested** $\alpha$ syntax](syntax/core), and here is a reference to another page (`[](../intro.md)`): [](../intro.md).
```{note}
If you wish to have the target's title inserted into your text, you can
leave the "text" section of the markdown link empty. For example, this
markdown: `[](syntax.md)` will result in: [](syntax.md).
```
(syntax/code-blocks)=
## Code syntax highlighting
Code blocks contain a language identifier, which is used to determine the language of the code.
This language is used to determine the syntax highlighting, using an available [pygments lexer](https://pygments.org/docs/lexers/).
````markdown
```python
from a import b
c = "string"
```
````
```python
from a import b
c = "string"
```
You can create and register your own lexer, using the [`pygments.lexers` entry point](https://pygments.org/docs/plugins/#register-plugins),
or within a sphinx extension, with the [`app.add_lexer` method](sphinx:sphinx.application.Sphinx.add_lexer).
Using the `myst_number_code_blocks` configuration option, you can also control whether code blocks are numbered by line.
For example, using `myst_number_code_blocks = ["typescript"]`:
```typescript
type MyBool = true | false;
interface User {
name: string;
id: number;
}
```
### Show backticks inside raw markdown blocks
If you'd like to show backticks inside of your markdown, you can do so by nesting them
in backticks of a greater length. Markdown will treat the outer-most backticks as the
edges of the "raw" block and everything inside will show up. For example:
``` `` `hi` `` ``` will be rendered as: `` `hi` ``
and
`````
````
```
hi
```
````
`````
will be rendered as:
````
```
hi
```
````
## Tables
Tables can be written using the standard [Github Flavoured Markdown syntax](https://github.github.com/gfm/#tables-extension-):
```md
| foo | bar |
| --- | --- |
| baz | bim |
```
| foo | bar |
| --- | --- |
| baz | bim |
Cells in a column can be aligned using the `:` character:
```md
| left | center | right |
| :--- | :----: | ----: |
| a | b | c |
```
| left | center | right |
| :--- | :----: | ----: |
| a | b | c |
:::{note}
Text is aligned by assigning `text-left`, `text-center`, or `text-right` to the cell.
It is then necessary for the theme you are using to include the appropriate css styling.
```html
<table class="colwidths-auto table">
<thead>
<tr><th class="text-left head"><p>left</p></th></tr>
</thead>
<tbody>
<tr><td class="text-left"><p>a</p></td></tr>
</tbody>
</table>
```
:::
## Images
MyST provides a few different syntaxes for including images in your documentation.
The standard Markdown syntax is:
```md
![fishy](img/fun-fish.png)
```
![fishy](img/fun-fish.png)
But you can also enable extended image syntaxes, to control attributes like width and captions.
See the [extended image syntax guide](syntax/images).
(syntax/footnotes)=
## Footnotes
Footnotes use the [pandoc specification](https://pandoc.org/MANUAL.html#footnotes).
Their labels **start with `^`** and can then be any alphanumeric string (no spaces), which is case-insensitive.
- If the label is an integer, then it will always use that integer for the rendered label (i.e. they are manually numbered).
- For any other labels, they will be auto-numbered in the order which they are referenced, skipping any manually numbered labels.
All footnote definitions are collected, and displayed at the bottom of the page (in the order they are referenced).
Note that un-referenced footnote definitions will not be displayed.
```md
- This is a manually-numbered footnote reference.[^3]
- This is an auto-numbered footnote reference.[^myref]
[^myref]: This is an auto-numbered footnote definition.
[^3]: This is a manually-numbered footnote definition.
```
- This is a manually-numbered footnote reference.[^3]
- This is an auto-numbered footnote reference.[^myref]
[^myref]: This is an auto-numbered footnote definition.
[^3]: This is a manually-numbered footnote definition.
Any preceding text after a footnote definitions, which is
indented by four or more spaces, will also be included in the footnote definition, and the text is rendered as MyST Markdown, e.g.
```md
A longer footnote definition.[^mylongdef]
[^mylongdef]: This is the _**footnote definition**_.
That continues for all indented lines
- even other block elements
Plus any preceding unindented lines,
that are not separated by a blank line
This is not part of the footnote.
```
A longer footnote definition.[^mylongdef]
[^mylongdef]: This is the _**footnote definition**_.
That continues for all indented lines
- even other block elements
Plus any subsequent unindented lines,
that are not separated by a blank line
This is not part of the footnote.
````{important}
Although footnote references can be used just fine within directives, e.g.[^myref],
it is recommended that footnote definitions are not set within directives,
unless they will only be referenced within that same directive:
```md
[^other]
[^other]: A definition within a directive
```
[^other]
[^other]: A definition within a directive
This is because, in the current implementation, they may not be available to reference in text above that particular directive.
````
By default, a transition line (with a `footnotes` class) will be placed before any footnotes.
This can be turned off by adding `myst_footnote_transition = False` to the config file.

2
example-include.md Normal file
View File

@ -0,0 +1,2 @@
[Used in how-to](docs/faq/index.md)
![alt](docs/_static/logo-wide.svg)

10
myst_parser/__init__.py Normal file
View File

@ -0,0 +1,10 @@
"""An extended commonmark compliant parser, with bridges to docutils & sphinx."""
__version__ = "0.18.1"
def setup(app):
"""Initialize the Sphinx extension."""
from myst_parser.sphinx_ext.main import setup_sphinx
setup_sphinx(app, load_parser=True)
return {"version": __version__, "parallel_read_safe": True}

11
myst_parser/_compat.py Normal file
View File

@ -0,0 +1,11 @@
"""Helpers for cross compatibility across dependency versions."""
from typing import Callable, Iterable
from docutils.nodes import Element
def findall(node: Element) -> Callable[..., Iterable[Element]]:
"""Iterate through"""
# findall replaces traverse in docutils v0.18
# note a difference is that findall is an iterator
return getattr(node, "findall", node.traverse)

198
myst_parser/_docs.py Normal file
View File

@ -0,0 +1,198 @@
"""Code to use internally, for documentation."""
from __future__ import annotations
import io
from typing import Sequence, Union
from docutils import nodes
from docutils.frontend import OptionParser
from docutils.parsers.rst import directives
from sphinx.directives import other
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective
from typing_extensions import get_args, get_origin
from .config.main import MdParserConfig
from .parsers.docutils_ import Parser as DocutilsParser
logger = logging.getLogger(__name__)
class _ConfigBase(SphinxDirective):
"""Directive to automate rendering of the configuration."""
@staticmethod
def table_header():
return [
"```````{list-table}",
":header-rows: 1",
":widths: 15 10 20",
"",
"* - Name",
" - Type",
" - Description",
]
@staticmethod
def field_default(value):
default = " ".join(f"{value!r}".splitlines())
return default
@staticmethod
def field_type(field):
ftypes: Sequence[str]
if get_origin(field.type) is Union:
ftypes = get_args(field.type)
else:
ftypes = [field.type]
ctype = " | ".join(
str("None" if ftype == type(None) else ftype) # type: ignore # noqa: E721
for ftype in ftypes
)
ctype = " ".join(ctype.splitlines())
ctype = ctype.replace("typing.", "")
ctype = ctype.replace("typing_extensions.", "")
for tname in ("str", "int", "float", "bool"):
ctype = ctype.replace(f"<class '{tname}'>", tname)
return ctype
class MystConfigDirective(_ConfigBase):
option_spec = {
"sphinx": directives.flag,
"extensions": directives.flag,
"scope": lambda x: directives.choice(x, ["global", "local"]),
}
def run(self):
"""Run the directive."""
config = MdParserConfig()
text = self.table_header()
count = 0
for name, value, field in config.as_triple():
# filter by sphinx options
if "sphinx" in self.options and field.metadata.get("sphinx_exclude"):
continue
if "extensions" in self.options:
if not field.metadata.get("extension"):
continue
else:
if field.metadata.get("extension"):
continue
if self.options.get("scope") == "local":
if field.metadata.get("global_only"):
continue
if self.options.get("scope") == "global":
name = f"myst_{name}"
description = " ".join(field.metadata.get("help", "").splitlines())
if field.metadata.get("extension"):
description = f"{field.metadata.get('extension')}: {description}"
default = self.field_default(value)
ctype = self.field_type(field)
text.extend(
[
f"* - `{name}`",
f" - `{ctype}`",
f" - {description} (default: `{default}`)",
]
)
count += 1
if not count:
return []
text.append("```````")
node = nodes.Element()
self.state.nested_parse(text, 0, node)
return node.children
class DocutilsCliHelpDirective(SphinxDirective):
"""Directive to print the docutils CLI help."""
has_content = False
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
def run(self):
"""Run the directive."""
stream = io.StringIO()
OptionParser(
components=(DocutilsParser,),
usage="myst-docutils-<writer> [options] [<source> [<destination>]]",
).print_help(stream)
return [nodes.literal_block("", stream.getvalue())]
class DirectiveDoc(SphinxDirective):
"""Load and document a directive."""
required_arguments = 1 # name of the directive
has_content = True
def run(self):
"""Run the directive."""
name = self.arguments[0]
# load the directive class
klass, _ = directives.directive(
name, self.state.memo.language, self.state.document
)
if klass is None:
logger.warning(f"Directive {name} not found.", line=self.lineno)
return []
content = " ".join(self.content)
text = f"""\
:Name: `{name}`
:Description: {content}
:Arguments: {klass.required_arguments} required, {klass.optional_arguments} optional
:Content: {'yes' if klass.has_content else 'no'}
:Options:
"""
if klass.option_spec:
text += " name | type\n -----|------\n"
for key, func in klass.option_spec.items():
text += f" {key} | {convert_opt(name, func)}\n"
node = nodes.Element()
self.state.nested_parse(text.splitlines(), 0, node)
return node.children
def convert_opt(name, func):
"""Convert an option function to a string."""
if func is directives.flag:
return "flag"
if func is directives.unchanged:
return "text"
if func is directives.unchanged_required:
return "text"
if func is directives.class_option:
return "space-delimited list"
if func is directives.uri:
return "URI"
if func is directives.path:
return "path"
if func is int:
return "integer"
if func is directives.positive_int:
return "integer (positive)"
if func is directives.nonnegative_int:
return "integer (non-negative)"
if func is directives.positive_int_list:
return "space/comma-delimited list of integers (positive)"
if func is directives.percentage:
return "percentage"
if func is directives.length_or_unitless:
return "length or unitless"
if func is directives.length_or_percentage_or_unitless:
return "length, percentage or unitless"
if func is other.int_or_nothing:
return "integer"
return ""

42
myst_parser/cli.py Normal file
View File

@ -0,0 +1,42 @@
import argparse
import sys
from markdown_it.renderer import RendererHTML
from myst_parser.config.main import MdParserConfig
from myst_parser.parsers.mdit import create_md_parser
def print_anchors(args=None):
""" """
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument(
"input",
nargs="?",
type=argparse.FileType("r", encoding="utf8"),
default=sys.stdin,
help="Input file (default stdin)",
)
arg_parser.add_argument(
"-o",
"--output",
type=argparse.FileType("w", encoding="utf8"),
default=sys.stdout,
help="Output file (default stdout)",
)
arg_parser.add_argument(
"-l", "--level", type=int, default=2, help="Maximum heading level."
)
args = arg_parser.parse_args(args)
parser = create_md_parser(MdParserConfig(heading_anchors=args.level), RendererHTML)
def _filter_plugin(state):
state.tokens = [
t
for t in state.tokens
if t.type.startswith("heading_") and int(t.tag[1]) <= args.level
]
parser.use(lambda p: p.core.ruler.push("filter", _filter_plugin))
text = parser.render(args.input.read())
args.output.write(text)

View File

@ -0,0 +1 @@
"""This module holds the global configuration for the parser ``MdParserConfig``."""

View File

@ -0,0 +1,161 @@
"""Validators for dataclasses, mirroring those of https://github.com/python-attrs/attrs."""
from __future__ import annotations
import dataclasses as dc
from typing import Any, Sequence
from typing_extensions import Protocol
def validate_field(inst: Any, field: dc.Field, value: Any) -> None:
"""Validate the field of a dataclass,
according to a `validator` function set in the field.metadata.
The validator function should take as input (inst, field, value) and
raise an exception if the value is invalid.
"""
if "validator" not in field.metadata:
return
if isinstance(field.metadata["validator"], list):
for validator in field.metadata["validator"]:
validator(inst, field, value)
else:
field.metadata["validator"](inst, field, value)
def validate_fields(inst: Any) -> None:
"""Validate the fields of a dataclass,
according to `validator` functions set in the field metadata.
This function should be called in the `__post_init__` of the dataclass.
The validator function should take as input (inst, field, value) and
raise an exception if the value is invalid.
"""
for field in dc.fields(inst):
validate_field(inst, field, getattr(inst, field.name))
class ValidatorType(Protocol):
def __call__(
self, inst: bytes, field: dc.Field, value: Any, suffix: str = ""
) -> None:
...
def instance_of(type: type[Any] | tuple[type[Any], ...]) -> ValidatorType:
"""
A validator that raises a `TypeError` if the initializer is called
with a wrong type for this particular attribute (checks are performed using
`isinstance` therefore it's also valid to pass a tuple of types).
:param type: The type to check for.
"""
def _validator(inst, field, value, suffix=""):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not isinstance(value, type):
raise TypeError(
f"'{field.name}{suffix}' must be of type {type!r} "
f"(got {value!r} that is a {value.__class__!r})."
)
return _validator
def optional(validator: ValidatorType) -> ValidatorType:
"""
A validator that makes an attribute optional. An optional attribute is one
which can be set to ``None`` in addition to satisfying the requirements of
the sub-validator.
"""
def _validator(inst, field, value, suffix=""):
if value is None:
return
validator(inst, field, value, suffix=suffix)
return _validator
def is_callable(inst, field, value, suffix=""):
"""
A validator that raises a `TypeError` if the
initializer is called with a value for this particular attribute
that is not callable.
"""
if not callable(value):
raise TypeError(
f"'{field.name}{suffix}' must be callable "
f"(got {value!r} that is a {value.__class__!r})."
)
def in_(options: Sequence) -> ValidatorType:
"""
A validator that raises a `ValueError` if the initializer is called
with a value that does not belong in the options provided. The check is
performed using ``value in options``.
:param options: Allowed options.
"""
def _validator(inst, field, value, suffix=""):
try:
in_options = value in options
except TypeError: # e.g. `1 in "abc"`
in_options = False
if not in_options:
raise ValueError(
f"'{field.name}{suffix}' must be in {options!r} (got {value!r})"
)
return _validator
def deep_iterable(
member_validator: ValidatorType, iterable_validator: ValidatorType | None = None
) -> ValidatorType:
"""
A validator that performs deep validation of an iterable.
:param member_validator: Validator to apply to iterable members
:param iterable_validator: Validator to apply to iterable itself
"""
def _validator(inst, field, value, suffix=""):
if iterable_validator is not None:
iterable_validator(inst, field, value, suffix=suffix)
for idx, member in enumerate(value):
member_validator(inst, field, member, suffix=f"{suffix}[{idx}]")
return _validator
def deep_mapping(
key_validator: ValidatorType,
value_validator: ValidatorType,
mapping_validator: ValidatorType | None = None,
) -> ValidatorType:
"""
A validator that performs deep validation of a dictionary.
:param key_validator: Validator to apply to dictionary keys
:param value_validator: Validator to apply to dictionary values
:param mapping_validator: Validator to apply to top-level mapping attribute (optional)
"""
def _validator(inst, field: dc.Field, value, suffix=""):
if mapping_validator is not None:
mapping_validator(inst, field, value)
for key in value:
key_validator(inst, field, key, suffix=f"{suffix}[{key!r}]")
value_validator(inst, field, value[key], suffix=f"{suffix}[{key!r}]")
return _validator

409
myst_parser/config/main.py Normal file
View File

@ -0,0 +1,409 @@
"""The configuration for the myst parser."""
import dataclasses as dc
from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from .dc_validators import (
deep_iterable,
deep_mapping,
in_,
instance_of,
is_callable,
optional,
validate_field,
validate_fields,
)
def check_extensions(_, __, value):
if not isinstance(value, Iterable):
raise TypeError(f"'enable_extensions' not iterable: {value}")
diff = set(value).difference(
[
"amsmath",
"attrs_image",
"colon_fence",
"deflist",
"dollarmath",
"fieldlist",
"html_admonition",
"html_image",
"linkify",
"replacements",
"smartquotes",
"strikethrough",
"substitution",
"tasklist",
]
)
if diff:
raise ValueError(f"'enable_extensions' items not recognised: {diff}")
def check_sub_delimiters(_, __, value):
if (not isinstance(value, (tuple, list))) or len(value) != 2:
raise TypeError(f"myst_sub_delimiters is not a tuple of length 2: {value}")
for delim in value:
if (not isinstance(delim, str)) or len(delim) != 1:
raise TypeError(
f"myst_sub_delimiters does not contain strings of length 1: {value}"
)
@dc.dataclass()
class MdParserConfig:
"""Configuration options for the Markdown Parser.
Note in the sphinx configuration these option names are prepended with ``myst_``
"""
# TODO replace commonmark_only, gfm_only with a single option
commonmark_only: bool = dc.field(
default=False,
metadata={
"validator": instance_of(bool),
"help": "Use strict CommonMark parser",
},
)
gfm_only: bool = dc.field(
default=False,
metadata={
"validator": instance_of(bool),
"help": "Use strict Github Flavoured Markdown parser",
},
)
enable_extensions: Sequence[str] = dc.field(
default_factory=list,
metadata={"validator": check_extensions, "help": "Enable syntax extensions"},
)
disable_syntax: Iterable[str] = dc.field(
default_factory=list,
metadata={
"validator": deep_iterable(instance_of(str), instance_of((list, tuple))),
"help": "Disable Commonmark syntax elements",
},
)
all_links_external: bool = dc.field(
default=False,
metadata={
"validator": instance_of(bool),
"help": "Parse all links as simple hyperlinks",
},
)
# see https://en.wikipedia.org/wiki/List_of_URI_schemes
url_schemes: Optional[Iterable[str]] = dc.field(
default=cast(Optional[Iterable[str]], ("http", "https", "mailto", "ftp")),
metadata={
"validator": optional(
deep_iterable(instance_of(str), instance_of((list, tuple)))
),
"help": "URL scheme prefixes identified as external links",
},
)
ref_domains: Optional[Iterable[str]] = dc.field(
default=None,
metadata={
"validator": optional(
deep_iterable(instance_of(str), instance_of((list, tuple)))
),
"help": "Sphinx domain names to search in for link references",
},
)
highlight_code_blocks: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Syntax highlight code blocks with pygments",
"docutils_only": True,
},
)
number_code_blocks: Sequence[str] = dc.field(
default_factory=list,
metadata={
"validator": deep_iterable(instance_of(str), instance_of((list, tuple))),
"help": "Add line numbers to code blocks with these languages",
},
)
title_to_header: bool = dc.field(
default=False,
metadata={
"validator": instance_of(bool),
"help": "Convert a `title` field in the top-matter to a H1 header",
},
)
heading_anchors: Optional[int] = dc.field(
default=None,
metadata={
"validator": optional(in_([1, 2, 3, 4, 5, 6, 7])),
"help": "Heading level depth to assign HTML anchors",
},
)
heading_slug_func: Optional[Callable[[str], str]] = dc.field(
default=None,
metadata={
"validator": optional(is_callable),
"help": "Function for creating heading anchors",
"global_only": True,
},
)
html_meta: Dict[str, str] = dc.field(
default_factory=dict,
repr=False,
metadata={
"validator": deep_mapping(
instance_of(str), instance_of(str), instance_of(dict)
),
"merge_topmatter": True,
"help": "HTML meta tags",
},
)
footnote_transition: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Place a transition before any footnotes",
},
)
words_per_minute: int = dc.field(
default=200,
metadata={
"validator": instance_of(int),
"help": "For reading speed calculations",
},
)
# Extension specific
substitutions: Dict[str, Union[str, int, float]] = dc.field(
default_factory=dict,
repr=False,
metadata={
"validator": deep_mapping(
instance_of(str), instance_of((str, int, float)), instance_of(dict)
),
"merge_topmatter": True,
"help": "Substitutions mapping",
"extension": "substitutions",
},
)
sub_delimiters: Tuple[str, str] = dc.field(
default=("{", "}"),
metadata={
"validator": check_sub_delimiters,
"help": "Substitution delimiters",
"extension": "substitutions",
},
)
linkify_fuzzy_links: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Recognise URLs without schema prefixes",
"extension": "linkify",
},
)
dmath_allow_labels: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Parse `$$...$$ (label)`",
"extension": "dollarmath",
},
)
dmath_allow_space: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Allow initial/final spaces in `$ ... $`",
"extension": "dollarmath",
},
)
dmath_allow_digits: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Allow initial/final digits `1$ ...$2`",
"extension": "dollarmath",
},
)
dmath_double_inline: bool = dc.field(
default=False,
metadata={
"validator": instance_of(bool),
"help": "Parse inline `$$ ... $$`",
"extension": "dollarmath",
},
)
update_mathjax: bool = dc.field(
default=True,
metadata={
"validator": instance_of(bool),
"help": "Update sphinx.ext.mathjax configuration to ignore `$` delimiters",
"extension": "dollarmath",
"global_only": True,
},
)
mathjax_classes: str = dc.field(
default="tex2jax_process|mathjax_process|math|output_area",
metadata={
"validator": instance_of(str),
"help": "MathJax classes to add to math HTML",
"extension": "dollarmath",
"global_only": True,
},
)
def __post_init__(self):
validate_fields(self)
def copy(self, **kwargs: Any) -> "MdParserConfig":
"""Return a new object replacing specified fields with new values.
Note: initiating the copy will also validate the new fields.
"""
return dc.replace(self, **kwargs)
@classmethod
def get_fields(cls) -> Tuple[dc.Field, ...]:
"""Return all attribute fields in this class."""
return dc.fields(cls)
def as_dict(self, dict_factory=dict) -> dict:
"""Return a dictionary of field name -> value."""
return dc.asdict(self, dict_factory=dict_factory)
def as_triple(self) -> Iterable[Tuple[str, Any, dc.Field]]:
"""Yield triples of (name, value, field)."""
fields = {f.name: f for f in dc.fields(self.__class__)}
for name, value in dc.asdict(self).items():
yield name, value, fields[name]
def merge_file_level(
config: MdParserConfig,
topmatter: Dict[str, Any],
warning: Callable[[str, str], None],
) -> MdParserConfig:
"""Merge the file-level topmatter with the global config.
:param config: Global config.
:param topmatter: Topmatter from the file.
:param warning: Function to call with a warning (type, message).
:returns: A new config object
"""
# get updates
updates: Dict[str, Any] = {}
myst = topmatter.get("myst", {})
if not isinstance(myst, dict):
warning("topmatter", f"'myst' key not a dict: {type(myst)}")
else:
updates = myst
# allow html_meta and substitutions at top-level for back-compatibility
if "html_meta" in topmatter:
warning(
"topmatter",
"top-level 'html_meta' key is deprecated, "
"place under 'myst' key instead",
)
updates["html_meta"] = topmatter["html_meta"]
if "substitutions" in topmatter:
warning(
"topmatter",
"top-level 'substitutions' key is deprecated, "
"place under 'myst' key instead",
)
updates["substitutions"] = topmatter["substitutions"]
new = config.copy()
# validate each update
fields = {name: (value, field) for name, value, field in config.as_triple()}
for name, value in updates.items():
if name not in fields:
warning("topmatter", f"Unknown field: {name}")
continue
old_value, field = fields[name]
try:
validate_field(new, field, value)
except Exception as exc:
warning("topmatter", str(exc))
continue
if field.metadata.get("merge_topmatter"):
value = {**old_value, **value}
setattr(new, name, value)
return new
class TopmatterReadError(Exception):
"""Topmatter parsing error."""
def read_topmatter(text: Union[str, Iterator[str]]) -> Optional[Dict[str, Any]]:
"""Read the (optional) YAML topmatter from a source string.
This is identified by the first line starting with `---`,
then read up to a terminating line of `---`, or `...`.
:param source: The source string to read from
:return: The topmatter
"""
import yaml
if isinstance(text, str):
if not text.startswith("---"): # skip creating the line list in memory
return None
text = (line for line in text.splitlines())
try:
if not next(text).startswith("---"):
return None
except StopIteration:
return None
top_matter = []
for line in text:
if line.startswith("---") or line.startswith("..."):
break
top_matter.append(line.rstrip() + "\n")
try:
metadata = yaml.safe_load("".join(top_matter))
assert isinstance(metadata, dict)
except (yaml.parser.ParserError, yaml.scanner.ScannerError) as err:
raise TopmatterReadError("Malformed YAML") from err
if not isinstance(metadata, dict):
raise TopmatterReadError(f"YAML is not a dict: {type(metadata)}")
return metadata

6
myst_parser/docutils_.py Normal file
View File

@ -0,0 +1,6 @@
"""A module for compatibility with the docutils>=0.17 `include` directive, in RST documents::
.. include:: path/to/file.md
:parser: myst_parser.docutils_
"""
from myst_parser.parsers.docutils_ import Parser # noqa: F401

View File

@ -0,0 +1 @@
"""Conversion of Markdown-it tokens to docutils AST."""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,139 @@
"""Convert HTML to docutils nodes."""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from docutils import nodes
from myst_parser.parsers.parse_html import Data, tokenize_html
if TYPE_CHECKING:
from .base import DocutilsRenderer
def make_error(
document: nodes.document, error_msg: str, text: str, line_number: int
) -> nodes.system_message:
return document.reporter.error(
error_msg,
nodes.literal_block(text, text),
line=line_number,
)
OPTION_KEYS_IMAGE = {"class", "alt", "height", "width", "align", "name"}
# note: docutils also has scale and target
OPTION_KEYS_ADMONITION = {"class", "name"}
# See https://github.com/micromark/micromark-extension-gfm-tagfilter
RE_FLOW = re.compile(
r"<(\/?)(iframe|noembed|noframes|plaintext|script|style|title|textarea|xmp)(?=[\t\n\f\r />])",
re.IGNORECASE,
)
def default_html(text: str, source: str, line_number: int) -> list[nodes.Element]:
raw_html = nodes.raw("", text, format="html")
raw_html.source = source
raw_html.line = line_number
return [raw_html]
def html_to_nodes(
text: str, line_number: int, renderer: DocutilsRenderer
) -> list[nodes.Element]:
"""Convert HTML to docutils nodes."""
if renderer.md_config.gfm_only:
text, _ = RE_FLOW.subn(lambda s: s.group(0).replace("<", "&lt;"), text)
enable_html_img = "html_image" in renderer.md_config.enable_extensions
enable_html_admonition = "html_admonition" in renderer.md_config.enable_extensions
if not (enable_html_img or enable_html_admonition):
return default_html(text, renderer.document["source"], line_number)
# parse the HTML to AST
try:
root = tokenize_html(text).strip(inplace=True, recurse=False)
except Exception:
msg_node = renderer.create_warning(
"HTML could not be parsed", line=line_number, subtype="html"
)
return ([msg_node] if msg_node else []) + default_html(
text, renderer.document["source"], line_number
)
if len(root) < 1:
# if empty
return default_html(text, renderer.document["source"], line_number)
if not all(
(enable_html_img and child.name == "img")
or (
enable_html_admonition
and child.name == "div"
and "admonition" in child.attrs.classes
)
for child in root
):
return default_html(text, renderer.document["source"], line_number)
nodes_list = []
for child in root:
if child.name == "img":
if "src" not in child.attrs:
return [
renderer.reporter.error(
"<img> missing 'src' attribute", line=line_number
)
]
content = "\n".join(
f":{k}: {v}"
for k, v in sorted(child.attrs.items())
if k in OPTION_KEYS_IMAGE
)
nodes_list.extend(
renderer.run_directive(
"image", child.attrs["src"], content, line_number
)
)
else:
children = child.strip().children
if (
children
and children[0].name in ("div", "p")
and (
"title" in children[0].attrs.classes
or "admonition-title" in children[0].attrs.classes
)
):
title = "".join(child.render() for child in children.pop(0))
else:
title = "Note"
options = "\n".join(
f":{k}: {v}"
for k, v in sorted(child.attrs.items())
if k in OPTION_KEYS_ADMONITION
).rstrip()
new_children = []
for child in children:
if child.name == "p":
new_children.extend(child.children)
new_children.append(Data("\n\n"))
else:
new_children.append(child)
content = (
options
+ ("\n\n" if options else "")
+ "".join(child.render() for child in new_children).lstrip()
)
nodes_list.extend(
renderer.run_directive("admonition", title, content, line_number)
)
return nodes_list

View File

@ -0,0 +1,245 @@
"""Convert Markdown-it tokens to docutils nodes, including sphinx specific elements."""
from __future__ import annotations
import os
from pathlib import Path
from typing import cast
from urllib.parse import unquote
from uuid import uuid4
from docutils import nodes
from markdown_it.tree import SyntaxTreeNode
from sphinx import addnodes
from sphinx.domains.math import MathDomain
from sphinx.domains.std import StandardDomain
from sphinx.environment import BuildEnvironment
from sphinx.util import logging
from sphinx.util.nodes import clean_astext
from myst_parser.mdit_to_docutils.base import DocutilsRenderer
LOGGER = logging.getLogger(__name__)
def create_warning(
document: nodes.document,
message: str,
*,
line: int | None = None,
append_to: nodes.Element | None = None,
wtype: str = "myst",
subtype: str = "other",
) -> nodes.system_message | None:
"""Generate a warning, logging it if necessary.
If the warning type is listed in the ``suppress_warnings`` configuration,
then ``None`` will be returned and no warning logged.
"""
message = f"{message} [{wtype}.{subtype}]"
kwargs = {"line": line} if line is not None else {}
if logging.is_suppressed_warning(
wtype, subtype, document.settings.env.app.config.suppress_warnings
):
return None
msg_node = document.reporter.warning(message, **kwargs)
if append_to is not None:
append_to.append(msg_node)
return None
class SphinxRenderer(DocutilsRenderer):
"""A markdown-it-py renderer to populate (in-place) a `docutils.document` AST.
This is sub-class of `DocutilsRenderer` that handles sphinx specific aspects,
such as cross-referencing.
"""
@property
def doc_env(self) -> BuildEnvironment:
return self.document.settings.env
def create_warning(
self,
message: str,
*,
line: int | None = None,
append_to: nodes.Element | None = None,
wtype: str = "myst",
subtype: str = "other",
) -> nodes.system_message | None:
"""Generate a warning, logging it if necessary.
If the warning type is listed in the ``suppress_warnings`` configuration,
then ``None`` will be returned and no warning logged.
"""
return create_warning(
self.document,
message,
line=line,
append_to=append_to,
wtype=wtype,
subtype=subtype,
)
def render_internal_link(self, token: SyntaxTreeNode) -> None:
"""Render link token `[text](link "title")`,
where the link has not been identified as an external URL.
"""
destination = unquote(cast(str, token.attrGet("href") or ""))
# make the path relative to an "including" document
# this is set when using the `relative-docs` option of the MyST `include` directive
relative_include = self.md_env.get("relative-docs", None)
if relative_include is not None and destination.startswith(relative_include[0]):
source_dir, include_dir = relative_include[1:]
destination = os.path.relpath(
os.path.join(include_dir, os.path.normpath(destination)), source_dir
)
potential_path = (
Path(self.doc_env.doc2path(self.doc_env.docname)).parent / destination
if self.doc_env.srcdir # not set in some test situations
else None
)
if (
potential_path
and potential_path.is_file()
and not any(
destination.endswith(suffix)
for suffix in self.doc_env.config.source_suffix
)
):
wrap_node = addnodes.download_reference(
refdoc=self.doc_env.docname,
reftarget=destination,
reftype="myst",
refdomain=None, # Added to enable cross-linking
refexplicit=len(token.children or []) > 0,
refwarn=False,
)
classes = ["xref", "download", "myst"]
text = destination if not token.children else ""
else:
wrap_node = addnodes.pending_xref(
refdoc=self.doc_env.docname,
reftarget=destination,
reftype="myst",
refdomain=None, # Added to enable cross-linking
refexplicit=len(token.children or []) > 0,
refwarn=True,
)
classes = ["xref", "myst"]
text = ""
self.add_line_and_source_path(wrap_node, token)
title = token.attrGet("title")
if title:
wrap_node["title"] = title
self.current_node.append(wrap_node)
inner_node = nodes.inline("", text, classes=classes)
wrap_node.append(inner_node)
with self.current_node_context(inner_node):
self.render_children(token)
def render_heading(self, token: SyntaxTreeNode) -> None:
"""This extends the docutils method, to allow for the addition of heading ids.
These ids are computed by the ``markdown-it-py`` ``anchors_plugin``
as "slugs" which are unique to a document.
The approach is similar to ``sphinx.ext.autosectionlabel``
"""
super().render_heading(token)
if not isinstance(self.current_node, nodes.section):
return
# create the slug string
slug = cast(str, token.attrGet("id"))
if slug is None:
return
section = self.current_node
doc_slug = self.doc_env.doc2path(self.doc_env.docname, base=False) + "#" + slug
# save the reference in the standard domain, so that it can be handled properly
domain = cast(StandardDomain, self.doc_env.get_domain("std"))
if doc_slug in domain.labels:
other_doc = self.doc_env.doc2path(domain.labels[doc_slug][0])
self.create_warning(
f"duplicate label {doc_slug}, other instance in {other_doc}",
line=section.line,
subtype="anchor",
)
labelid = section["ids"][0]
domain.anonlabels[doc_slug] = self.doc_env.docname, labelid
domain.labels[doc_slug] = (
self.doc_env.docname,
labelid,
clean_astext(section[0]),
)
self.doc_env.metadata[self.doc_env.docname]["myst_anchors"] = True
section["myst-anchor"] = doc_slug
def render_math_block_label(self, token: SyntaxTreeNode) -> None:
"""Render math with referencable labels, e.g. ``$a=1$ (label)``."""
label = token.info
content = token.content
node = nodes.math_block(
content, content, nowrap=False, number=None, label=label
)
target = self.add_math_target(node)
self.add_line_and_source_path(target, token)
self.current_node.append(target)
self.add_line_and_source_path(node, token)
self.current_node.append(node)
def _random_label(self) -> str:
return str(uuid4())
def render_amsmath(self, token: SyntaxTreeNode) -> None:
"""Renderer for the amsmath extension."""
# environment = token.meta["environment"]
content = token.content
if token.meta["numbered"] != "*":
# TODO how to parse and reference labels within environment?
# for now we give create a unique hash, so the equation will be numbered
# but there will be no reference clashes
label = self._random_label()
node = nodes.math_block(
content,
content,
nowrap=True,
number=None,
classes=["amsmath"],
label=label,
)
target = self.add_math_target(node)
self.add_line_and_source_path(target, token)
self.current_node.append(target)
else:
node = nodes.math_block(
content, content, nowrap=True, number=None, classes=["amsmath"]
)
self.add_line_and_source_path(node, token)
self.current_node.append(node)
def add_math_target(self, node: nodes.math_block) -> nodes.target:
# Code mainly copied from sphinx.directives.patches.MathDirective
# register label to domain
domain = cast(MathDomain, self.doc_env.get_domain("math"))
domain.note_equation(self.doc_env.docname, node["label"], location=node)
node["number"] = domain.get_equation_number_for(node["label"])
node["docname"] = self.doc_env.docname
# create target node
node_id = nodes.make_id("equation-%s" % node["label"])
target = nodes.target("", "", ids=[node_id])
self.document.note_explicit_target(target)
return target

View File

@ -0,0 +1,36 @@
import html
from typing import Iterable, Optional
from urllib.parse import quote, urlparse
def escape_url(raw: str) -> str:
"""
Escape urls to prevent code injection craziness. (Hopefully.)
"""
return html.escape(quote(html.unescape(raw), safe="/#:()*?=%@+,&"))
def is_external_url(
reference: str,
known_url_schemes: Optional[Iterable[str]],
match_fragment: bool = False,
) -> bool:
"""Return if a reference should be recognised as an external URL.
URLs are of the format: scheme://netloc/path;parameters?query#fragment
This checks if there is a url scheme (e.g. 'https') and, if so,
if the scheme is is the list of known_url_schemes (if supplied).
:param known_url_schemes: e.g. ["http", "https", "mailto"]
If None, match all schemes
:param match_fragment: If True and a fragment found, then True will be returned,
irrespective of a scheme match
"""
url_check = urlparse(reference)
if known_url_schemes is not None:
scheme_known = url_check.scheme in known_url_schemes
else:
scheme_known = bool(url_check.scheme)
return scheme_known or (match_fragment and url_check.fragment != "")

514
myst_parser/mocking.py Normal file
View File

@ -0,0 +1,514 @@
"""This module provides classes to Mock the core components of the docutils.RSTParser,
the key difference being that nested parsing treats the text as Markdown not rST.
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any
from docutils import nodes
from docutils.parsers.rst import Directive, DirectiveError
from docutils.parsers.rst import Parser as RSTParser
from docutils.parsers.rst.directives.misc import Include
from docutils.parsers.rst.states import Body, Inliner, RSTStateMachine
from docutils.statemachine import StringList
from docutils.utils import unescape
from .parsers.directives import parse_directive_text
if TYPE_CHECKING:
from .mdit_to_docutils.base import DocutilsRenderer
class MockingError(Exception):
"""An exception to signal an error during mocking of docutils components."""
class MockInliner:
"""A mock version of `docutils.parsers.rst.states.Inliner`.
This is parsed to role functions.
"""
def __init__(self, renderer: DocutilsRenderer):
"""Initialize the mock inliner."""
self._renderer = renderer
# here we mock that the `parse` method has already been called
# which is where these attributes are set (via the RST state Memo)
self.document = renderer.document
self.reporter = renderer.document.reporter
self.language = renderer.language_module_rst
self.parent = renderer.current_node
if not hasattr(self.reporter, "get_source_and_line"):
# In docutils this is set by `RSTState.runtime_init`
self.reporter.get_source_and_line = lambda l: (self.document["source"], l)
self.rfc_url = "rfc%d.html"
def problematic(
self, text: str, rawsource: str, message: nodes.system_message
) -> nodes.problematic:
"""Record a system message from parsing."""
msgid = self.document.set_id(message, self.parent)
problematic = nodes.problematic(rawsource, text, refid=msgid)
prbid = self.document.set_id(problematic)
message.add_backref(prbid)
return problematic
def parse(
self, text: str, lineno: int, memo: Any, parent: nodes.Node
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
"""Parse the text and return a list of nodes."""
# note the only place this is normally called,
# is by `RSTState.inline_text`, or in directives: `self.state.inline_text`,
# and there the state parses its own parent
# self.reporter = memo.reporter
# self.document = memo.document
# self.language = memo.language
with self._renderer.current_node_context(parent):
# the parent is never actually appended to though,
# so we make a temporary parent to parse into
container = nodes.Element()
with self._renderer.current_node_context(container):
self._renderer.nested_render_text(text, lineno, inline=True)
return container.children, []
def __getattr__(self, name: str):
"""This method is only be called if the attribute requested has not
been defined. Defined attributes will not be overridden.
"""
# TODO use document.reporter mechanism?
if hasattr(Inliner, name):
msg = "{cls} has not yet implemented attribute '{name}'".format(
cls=type(self).__name__, name=name
)
raise MockingError(msg).with_traceback(sys.exc_info()[2])
msg = f"{type(self).__name__} has no attribute {name}"
raise MockingError(msg).with_traceback(sys.exc_info()[2])
class MockState:
"""A mock version of `docutils.parsers.rst.states.RSTState`.
This is parsed to the `Directives.run()` method,
so that they may run nested parses on their content that will be parsed as markdown,
rather than RST.
"""
def __init__(
self,
renderer: DocutilsRenderer,
state_machine: MockStateMachine,
lineno: int,
):
self._renderer = renderer
self._lineno = lineno
self.document = renderer.document
self.reporter = renderer.document.reporter
self.state_machine = state_machine
self.inliner = MockInliner(renderer)
class Struct:
document = self.document
reporter = self.document.reporter
language = renderer.language_module_rst
title_styles: list[str] = []
section_level = max(renderer._level_to_elem)
section_bubble_up_kludge = False
inliner = self.inliner
self.memo = Struct
def parse_directive_block(
self,
content: StringList,
line_offset: int,
directive: type[Directive],
option_presets: dict,
) -> tuple[list, dict, StringList, int]:
"""Parse the full directive text
:returns: (arguments, options, content, content_offset)
"""
if option_presets:
raise MockingError("parse_directive_block: option_presets not implemented")
# TODO should argument_str always be ""?
arguments, options, body_lines, content_offset = parse_directive_text(
directive, "", "\n".join(content)
)
return (
arguments,
options,
StringList(body_lines, source=content.source),
line_offset + content_offset,
)
def nested_parse(
self,
block: StringList,
input_offset: int,
node: nodes.Element,
match_titles: bool = False,
state_machine_class=None,
state_machine_kwargs=None,
) -> None:
"""Perform a nested parse of the input block, with ``node`` as the parent.
:param block: The block of lines to parse.
:param input_offset: The offset of the first line of block,
to the starting line of the state (i.e. directive).
:param node: The parent node to attach the parsed content to.
:param match_titles: Whether to to allow the parsing of headings
(normally this is false,
since nested heading would break the document structure)
"""
sm_match_titles = self.state_machine.match_titles
with self._renderer.current_node_context(node):
self._renderer.nested_render_text(
"\n".join(block),
self._lineno + input_offset,
allow_headings=match_titles,
)
self.state_machine.match_titles = sm_match_titles
def parse_target(self, block, block_text, lineno: int):
"""
Taken from https://github.com/docutils-mirror/docutils/blob/e88c5fb08d5cdfa8b4ac1020dd6f7177778d5990/docutils/parsers/rst/states.py#L1927 # noqa: E501
"""
# Commenting out this code because it only applies to rST
# if block and block[-1].strip()[-1:] == "_": # possible indirect target
# reference = " ".join([line.strip() for line in block])
# refname = self.is_reference(reference)
# if refname:
# return "refname", refname
reference = "".join(["".join(line.split()) for line in block])
return "refuri", unescape(reference)
def inline_text(
self, text: str, lineno: int
) -> tuple[list[nodes.Element], list[nodes.Element]]:
"""Parse text with only inline rules.
:returns: (list of nodes, list of messages)
"""
return self.inliner.parse(text, lineno, self.memo, self._renderer.current_node)
# U+2014 is an em-dash:
attribution_pattern = re.compile("^((?:---?(?!-)|\u2014) *)(.+)")
def block_quote(self, lines: list[str], line_offset: int) -> list[nodes.Element]:
"""Parse a block quote, which is a block of text,
followed by an (optional) attribution.
::
No matter where you go, there you are.
-- Buckaroo Banzai
"""
elements = []
# split attribution
last_line_blank = False
blockquote_lines = lines
attribution_lines = []
attribution_line_offset = None
# First line after a blank line must begin with a dash
for i, line in enumerate(lines):
if not line.strip():
last_line_blank = True
continue
if not last_line_blank:
last_line_blank = False
continue
last_line_blank = False
match = self.attribution_pattern.match(line)
if not match:
continue
attribution_line_offset = i
attribution_lines = [match.group(2)]
for at_line in lines[i + 1 :]:
indented_line = at_line[len(match.group(1)) :]
if len(indented_line) != len(at_line.lstrip()):
break
attribution_lines.append(indented_line)
blockquote_lines = lines[:i]
break
# parse block
blockquote = nodes.block_quote()
self.nested_parse(blockquote_lines, line_offset, blockquote)
elements.append(blockquote)
# parse attribution
if attribution_lines:
attribution_text = "\n".join(attribution_lines)
lineno = self._lineno + line_offset + (attribution_line_offset or 0)
textnodes, messages = self.inline_text(attribution_text, lineno)
attribution = nodes.attribution(attribution_text, "", *textnodes)
(
attribution.source,
attribution.line,
) = self.state_machine.get_source_and_line(lineno)
blockquote += attribution
elements += messages
return elements
def build_table(self, tabledata, tableline, stub_columns: int = 0, widths=None):
return Body.build_table(self, tabledata, tableline, stub_columns, widths)
def build_table_row(self, rowdata, tableline):
return Body.build_table_row(self, rowdata, tableline)
def __getattr__(self, name: str):
"""This method is only be called if the attribute requested has not
been defined. Defined attributes will not be overridden.
"""
cls = type(self).__name__
if hasattr(Body, name):
msg = (
f"{cls} has not yet implemented attribute '{name}'. "
"You can parse RST directly via the `{eval-rst}` directive: "
"https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#how-directives-parse-content" # noqa: E501
)
else:
# The requested `name` is not a docutils Body element
# (such as "footnote", "block_quote", "paragraph", …)
msg = f"{cls} has no attribute '{name}'"
raise MockingError(msg).with_traceback(sys.exc_info()[2])
class MockStateMachine:
"""A mock version of `docutils.parsers.rst.states.RSTStateMachine`.
This is parsed to the `Directives.run()` method.
"""
def __init__(self, renderer: DocutilsRenderer, lineno: int):
self._renderer = renderer
self._lineno = lineno
self.document = renderer.document
self.language = renderer.language_module_rst
self.reporter = self.document.reporter
self.node: nodes.Element = renderer.current_node
self.match_titles: bool = True
def get_source(self, lineno: int | None = None):
"""Return document source path."""
return self.document["source"]
def get_source_and_line(self, lineno: int | None = None):
"""Return (source path, line) tuple for current or given line number."""
return self.document["source"], lineno or self._lineno
def __getattr__(self, name: str):
"""This method is only be called if the attribute requested has not
been defined. Defined attributes will not be overridden.
"""
if hasattr(RSTStateMachine, name):
msg = "{cls} has not yet implemented attribute '{name}'".format(
cls=type(self).__name__, name=name
)
raise MockingError(msg).with_traceback(sys.exc_info()[2])
msg = f"{type(self).__name__} has no attribute {name}"
raise MockingError(msg).with_traceback(sys.exc_info()[2])
class MockIncludeDirective:
"""This directive uses a lot of statemachine logic that is not yet mocked.
Therefore, we treat it as a special case (at least for now).
See:
https://docutils.sourceforge.io/docs/ref/rst/directives.html#including-an-external-document-fragment
"""
def __init__(
self,
renderer: DocutilsRenderer,
name: str,
klass: Include,
arguments: list,
options: dict,
body: list[str],
lineno: int,
):
self.renderer = renderer
self.document = renderer.document
self.name = name
self.klass = klass
self.arguments = arguments
self.options = options
self.body = body
self.lineno = lineno
def run(self) -> list[nodes.Element]:
from docutils.parsers.rst.directives.body import CodeBlock, NumberLines
if not self.document.settings.file_insertion_enabled:
raise DirectiveError(2, f'Directive "{self.name}" disabled.')
source_dir = Path(self.document["source"]).absolute().parent
include_arg = "".join([s.strip() for s in self.arguments[0].splitlines()])
if include_arg.startswith("<") and include_arg.endswith(">"):
# # docutils "standard" includes
path = Path(self.klass.standard_include_path).joinpath(include_arg[1:-1])
else:
# if using sphinx interpret absolute paths "correctly",
# i.e. relative to source directory
try:
sphinx_env = self.document.settings.env
except AttributeError:
pass
else:
_, include_arg = sphinx_env.relfn2path(self.arguments[0])
sphinx_env.note_included(include_arg)
path = Path(include_arg)
path = source_dir.joinpath(path)
# this ensures that the parent file is rebuilt if the included file changes
self.document.settings.record_dependencies.add(str(path))
# read file
encoding = self.options.get("encoding", self.document.settings.input_encoding)
error_handler = self.document.settings.input_encoding_error_handler
# tab_width = self.options.get("tab-width", self.document.settings.tab_width)
try:
file_content = path.read_text(encoding=encoding, errors=error_handler)
except Exception as error:
raise DirectiveError(
4,
'Directive "{}": error reading file: {}\n{}.'.format(
self.name, path, error
),
)
# get required section of text
startline = self.options.get("start-line", None)
endline = self.options.get("end-line", None)
file_content = "\n".join(file_content.splitlines()[startline:endline])
startline = startline or 0
for split_on_type in ["start-after", "end-before"]:
split_on = self.options.get(split_on_type, None)
if not split_on:
continue
split_index = file_content.find(split_on)
if split_index < 0:
raise DirectiveError(
4,
'Directive "{}"; option "{}": text not found "{}".'.format(
self.name, split_on_type, split_on
),
)
if split_on_type == "start-after":
startline += split_index + len(split_on)
file_content = file_content[split_index + len(split_on) :]
else:
file_content = file_content[:split_index]
if "literal" in self.options:
literal_block = nodes.literal_block(
file_content, source=str(path), classes=self.options.get("class", [])
)
literal_block.line = 1 # TODO don;t think this should be 1?
self.add_name(literal_block)
if "number-lines" in self.options:
try:
startline = int(self.options["number-lines"] or 1)
except ValueError:
raise DirectiveError(
3, ":number-lines: with non-integer " "start value"
)
endline = startline + len(file_content.splitlines())
if file_content.endswith("\n"):
file_content = file_content[:-1]
tokens = NumberLines([([], file_content)], startline, endline)
for classes, value in tokens:
if classes:
literal_block += nodes.inline(value, value, classes=classes)
else:
literal_block += nodes.Text(value)
else:
literal_block += nodes.Text(file_content)
return [literal_block]
if "code" in self.options:
self.options["source"] = str(path)
state_machine = MockStateMachine(self.renderer, self.lineno)
state = MockState(self.renderer, state_machine, self.lineno)
codeblock = CodeBlock(
name=self.name,
arguments=[self.options.pop("code")],
options=self.options,
content=file_content.splitlines(),
lineno=self.lineno,
content_offset=0,
block_text=file_content,
state=state,
state_machine=state_machine,
)
return codeblock.run()
# Here we perform a nested render, but temporarily setup the document/reporter
# with the correct document path and lineno for the included file.
source = self.renderer.document["source"]
rsource = self.renderer.reporter.source
line_func = getattr(self.renderer.reporter, "get_source_and_line", None)
try:
self.renderer.document["source"] = str(path)
self.renderer.reporter.source = str(path)
self.renderer.reporter.get_source_and_line = lambda l: (str(path), l)
if "relative-images" in self.options:
self.renderer.md_env["relative-images"] = os.path.relpath(
path.parent, source_dir
)
if "relative-docs" in self.options:
self.renderer.md_env["relative-docs"] = (
self.options["relative-docs"],
source_dir,
path.parent,
)
self.renderer.nested_render_text(
file_content, startline + 1, allow_headings=True
)
finally:
self.renderer.document["source"] = source
self.renderer.reporter.source = rsource
self.renderer.md_env.pop("relative-images", None)
self.renderer.md_env.pop("relative-docs", None)
if line_func is not None:
self.renderer.reporter.get_source_and_line = line_func
else:
del self.renderer.reporter.get_source_and_line
return []
def add_name(self, node: nodes.Element):
"""Append self.options['name'] to node['names'] if it exists.
Also normalize the name string and register it as explicit target.
"""
if "name" in self.options:
name = nodes.fully_normalize_name(self.options.pop("name"))
if "name" in node:
del node["name"]
node["names"].append(name)
self.renderer.document.note_explicit_target(node, node)
class MockRSTParser(RSTParser):
"""RSTParser which avoids a negative side effect."""
def parse(self, inputstring: str, document: nodes.document):
"""Parse the input to populate the document AST."""
from docutils.parsers.rst import roles
should_restore = False
if "" in roles._roles:
should_restore = True
blankrole = roles._roles[""]
super().parse(inputstring, document)
if should_restore:
roles._roles[""] = blankrole

View File

@ -0,0 +1 @@
"""Parsers of MyST Markdown source text to docutils AST."""

View File

@ -0,0 +1,190 @@
"""Fenced code blocks are parsed as directives,
if the block starts with ``{directive_name}``,
followed by arguments on the same line.
Directive options are read from a YAML block,
if the first content line starts with ``---``, e.g.
::
```{directive_name} arguments
---
option1: name
option2: |
Longer text block
---
content...
```
Or the option block will be parsed if the first content line starts with ``:``,
as a YAML block consisting of every line that starts with a ``:``, e.g.
::
```{directive_name} arguments
:option1: name
:option2: other
content...
```
If the first line of a directive's content is blank, this will be stripped
from the content.
This is to allow for separation between the option block and content.
"""
from __future__ import annotations
import datetime
import re
from textwrap import dedent
from typing import Any, Callable
import yaml
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives.misc import TestDirective
class DirectiveParsingError(Exception):
"""Raise on parsing/validation error."""
pass
def parse_directive_text(
directive_class: type[Directive],
first_line: str,
content: str,
validate_options: bool = True,
) -> tuple[list[str], dict, list[str], int]:
"""Parse (and validate) the full directive text.
:param first_line: The text on the same line as the directive name.
May be an argument or body text, dependent on the directive
:param content: All text after the first line. Can include options.
:param validate_options: Whether to validate the values of options
:returns: (arguments, options, body_lines, content_offset)
"""
if directive_class.option_spec:
body, options = parse_directive_options(
content, directive_class, validate=validate_options
)
body_lines = body.splitlines()
content_offset = len(content.splitlines()) - len(body_lines)
else:
# If there are no possible options, we do not look for a YAML block
options = {}
body_lines = content.splitlines()
content_offset = 0
if not (directive_class.required_arguments or directive_class.optional_arguments):
# If there are no possible arguments, then the body starts on the argument line
if first_line:
body_lines.insert(0, first_line)
arguments = []
else:
arguments = parse_directive_arguments(directive_class, first_line)
# remove first line of body if blank
# this is to allow space between the options and the content
if body_lines and not body_lines[0].strip():
body_lines = body_lines[1:]
content_offset += 1
# check for body content
if body_lines and not directive_class.has_content:
raise DirectiveParsingError("No content permitted")
return arguments, options, body_lines, content_offset
def parse_directive_options(
content: str, directive_class: type[Directive], validate: bool = True
):
"""Parse (and validate) the directive option section."""
options: dict[str, Any] = {}
if content.startswith("---"):
content = "\n".join(content.splitlines()[1:])
match = re.search(r"^-{3,}", content, re.MULTILINE)
if match:
yaml_block = content[: match.start()]
content = content[match.end() + 1 :] # TODO advance line number
else:
yaml_block = content
content = ""
yaml_block = dedent(yaml_block)
try:
options = yaml.safe_load(yaml_block) or {}
except (yaml.parser.ParserError, yaml.scanner.ScannerError) as error:
raise DirectiveParsingError("Invalid options YAML: " + str(error))
elif content.lstrip().startswith(":"):
content_lines = content.splitlines() # type: list
yaml_lines = []
while content_lines:
if not content_lines[0].lstrip().startswith(":"):
break
yaml_lines.append(content_lines.pop(0).lstrip()[1:])
yaml_block = "\n".join(yaml_lines)
content = "\n".join(content_lines)
try:
options = yaml.safe_load(yaml_block) or {}
except (yaml.parser.ParserError, yaml.scanner.ScannerError) as error:
raise DirectiveParsingError("Invalid options YAML: " + str(error))
if not isinstance(options, dict):
raise DirectiveParsingError(f"Invalid options (not dict): {options}")
if (not validate) or issubclass(directive_class, TestDirective):
# technically this directive spec only accepts one option ('option')
# but since its for testing only we accept all options
return content, options
# check options against spec
options_spec: dict[str, Callable] = directive_class.option_spec
for name, value in list(options.items()):
try:
convertor = options_spec[name]
except KeyError:
raise DirectiveParsingError(f"Unknown option: {name}")
if not isinstance(value, str):
if value is True or value is None:
value = None # flag converter requires no argument
elif isinstance(value, (int, float, datetime.date, datetime.datetime)):
# convertor always requires string input
value = str(value)
else:
raise DirectiveParsingError(
f'option "{name}" value not string (enclose with ""): {value}'
)
try:
converted_value = convertor(value)
except (ValueError, TypeError) as error:
raise DirectiveParsingError(
"Invalid option value: (option: '{}'; value: {})\n{}".format(
name, value, error
)
)
options[name] = converted_value
return content, options
def parse_directive_arguments(directive, arg_text):
"""Parse (and validate) the directive argument section."""
required = directive.required_arguments
optional = directive.optional_arguments
arguments = arg_text.split()
if len(arguments) < required:
raise DirectiveParsingError(
f"{required} argument(s) required, {len(arguments)} supplied"
)
elif len(arguments) > required + optional:
if directive.final_argument_whitespace:
arguments = arg_text.split(None, required + optional - 1)
else:
raise DirectiveParsingError(
"maximum {} argument(s) allowed, {} supplied".format(
required + optional, len(arguments)
)
)
return arguments

View File

@ -0,0 +1,275 @@
"""MyST Markdown parser for docutils."""
from dataclasses import Field
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
from docutils import frontend, nodes
from docutils.core import default_description, publish_cmdline
from docutils.parsers.rst import Parser as RstParser
from typing_extensions import Literal, get_args, get_origin
from myst_parser.config.main import (
MdParserConfig,
TopmatterReadError,
merge_file_level,
read_topmatter,
)
from myst_parser.mdit_to_docutils.base import DocutilsRenderer, create_warning
from myst_parser.parsers.mdit import create_md_parser
def _validate_int(
setting, value, option_parser, config_parser=None, config_section=None
) -> int:
"""Validate an integer setting."""
return int(value)
def _create_validate_tuple(length: int) -> Callable[..., Tuple[str, ...]]:
"""Create a validator for a tuple of length `length`."""
def _validate(
setting, value, option_parser, config_parser=None, config_section=None
):
string_list = frontend.validate_comma_separated_list(
setting, value, option_parser, config_parser, config_section
)
if len(string_list) != length:
raise ValueError(
f"Expecting {length} items in {setting}, got {len(string_list)}."
)
return tuple(string_list)
return _validate
class Unset:
"""A sentinel class for unset settings."""
def __repr__(self):
return "UNSET"
DOCUTILS_UNSET = Unset()
"""Sentinel for arguments not set through docutils.conf."""
DOCUTILS_EXCLUDED_ARGS = (
# docutils.conf can't represent callables
"heading_slug_func",
# docutils.conf can't represent dicts
"html_meta",
"substitutions",
# we can't add substitutions so not needed
"sub_delimiters",
# sphinx only options
"heading_anchors",
"ref_domains",
"update_mathjax",
"mathjax_classes",
)
"""Names of settings that cannot be set in docutils.conf."""
def _attr_to_optparse_option(at: Field, default: Any) -> Tuple[dict, str]:
"""Convert a field into a Docutils optparse options dict."""
if at.type is int:
return {"metavar": "<int>", "validator": _validate_int}, f"(default: {default})"
if at.type is bool:
return {
"metavar": "<boolean>",
"validator": frontend.validate_boolean,
}, f"(default: {default})"
if at.type is str:
return {
"metavar": "<str>",
}, f"(default: '{default}')"
if get_origin(at.type) is Literal and all(
isinstance(a, str) for a in get_args(at.type)
):
args = get_args(at.type)
return {
"metavar": f"<{'|'.join(repr(a) for a in args)}>",
"type": "choice",
"choices": args,
}, f"(default: {default!r})"
if at.type in (Iterable[str], Sequence[str]):
return {
"metavar": "<comma-delimited>",
"validator": frontend.validate_comma_separated_list,
}, f"(default: '{','.join(default)}')"
if at.type == Tuple[str, str]:
return {
"metavar": "<str,str>",
"validator": _create_validate_tuple(2),
}, f"(default: '{','.join(default)}')"
if at.type == Union[int, type(None)]:
return {
"metavar": "<null|int>",
"validator": _validate_int,
}, f"(default: {default})"
if at.type == Union[Iterable[str], type(None)]:
default_str = ",".join(default) if default else ""
return {
"metavar": "<null|comma-delimited>",
"validator": frontend.validate_comma_separated_list,
}, f"(default: {default_str!r})"
raise AssertionError(
f"Configuration option {at.name} not set up for use in docutils.conf."
)
def attr_to_optparse_option(
attribute: Field, default: Any, prefix: str = "myst_"
) -> Tuple[str, List[str], Dict[str, Any]]:
"""Convert an ``MdParserConfig`` attribute into a Docutils setting tuple.
:returns: A tuple of ``(help string, option flags, optparse kwargs)``.
"""
name = f"{prefix}{attribute.name}"
flag = "--" + name.replace("_", "-")
options = {"dest": name, "default": DOCUTILS_UNSET}
at_options, type_str = _attr_to_optparse_option(attribute, default)
options.update(at_options)
help_str = attribute.metadata.get("help", "") if attribute.metadata else ""
return (f"{help_str} {type_str}", [flag], options)
def create_myst_settings_spec(
excluded: Sequence[str], config_cls=MdParserConfig, prefix: str = "myst_"
):
"""Return a list of Docutils setting for the docutils MyST section."""
defaults = config_cls()
return tuple(
attr_to_optparse_option(at, getattr(defaults, at.name), prefix)
for at in config_cls.get_fields()
if at.name not in excluded
)
def create_myst_config(
settings: frontend.Values,
excluded: Sequence[str],
config_cls=MdParserConfig,
prefix: str = "myst_",
):
"""Create a configuration instance from the given settings."""
values = {}
for attribute in config_cls.get_fields():
if attribute.name in excluded:
continue
setting = f"{prefix}{attribute.name}"
val = getattr(settings, setting, DOCUTILS_UNSET)
if val is not DOCUTILS_UNSET:
values[attribute.name] = val
return config_cls(**values)
class Parser(RstParser):
"""Docutils parser for Markedly Structured Text (MyST)."""
supported: Tuple[str, ...] = ("md", "markdown", "myst")
"""Aliases this parser supports."""
settings_spec = (
"MyST options",
None,
create_myst_settings_spec(DOCUTILS_EXCLUDED_ARGS),
*RstParser.settings_spec,
)
"""Runtime settings specification."""
config_section = "myst parser"
config_section_dependencies = ("parsers",)
translate_section_name = None
def parse(self, inputstring: str, document: nodes.document) -> None:
"""Parse source text.
:param inputstring: The source string to parse
:param document: The root docutils node to add AST elements to
"""
self.setup_parse(inputstring, document)
# check for exorbitantly long lines
if hasattr(document.settings, "line_length_limit"):
for i, line in enumerate(inputstring.split("\n")):
if len(line) > document.settings.line_length_limit:
error = document.reporter.error(
f"Line {i+1} exceeds the line-length-limit:"
f" {document.settings.line_length_limit}."
)
document.append(error)
return
# create parsing configuration from the global config
try:
config = create_myst_config(document.settings, DOCUTILS_EXCLUDED_ARGS)
except Exception as exc:
error = document.reporter.error(f"Global myst configuration invalid: {exc}")
document.append(error)
config = MdParserConfig()
# update the global config with the file-level config
try:
topmatter = read_topmatter(inputstring)
except TopmatterReadError:
pass # this will be reported during the render
else:
if topmatter:
warning = lambda wtype, msg: create_warning( # noqa: E731
document, msg, line=1, append_to=document, subtype=wtype
)
config = merge_file_level(config, topmatter, warning)
# parse content
parser = create_md_parser(config, DocutilsRenderer)
parser.options["document"] = document
parser.render(inputstring)
# post-processing
# replace raw nodes if raw is not allowed
if not getattr(document.settings, "raw_enabled", True):
for node in document.traverse(nodes.raw):
warning = document.reporter.warning("Raw content disabled.")
node.parent.replace(node, warning)
self.finish_parse()
def _run_cli(writer_name: str, writer_description: str, argv: Optional[List[str]]):
"""Run the command line interface for a particular writer."""
publish_cmdline(
parser=Parser(),
writer_name=writer_name,
description=(
f"Generates {writer_description} from standalone MyST sources.\n{default_description}"
),
argv=argv,
)
def cli_html(argv: Optional[List[str]] = None) -> None:
"""Cmdline entrypoint for converting MyST to HTML."""
_run_cli("html", "(X)HTML documents", argv)
def cli_html5(argv: Optional[List[str]] = None):
"""Cmdline entrypoint for converting MyST to HTML5."""
_run_cli("html5", "HTML5 documents", argv)
def cli_latex(argv: Optional[List[str]] = None):
"""Cmdline entrypoint for converting MyST to LaTeX."""
_run_cli("latex", "LaTeX documents", argv)
def cli_xml(argv: Optional[List[str]] = None):
"""Cmdline entrypoint for converting MyST to XML."""
_run_cli("xml", "Docutils-native XML", argv)
def cli_pseudoxml(argv: Optional[List[str]] = None):
"""Cmdline entrypoint for converting MyST to pseudo-XML."""
_run_cli("pseudoxml", "pseudo-XML", argv)

123
myst_parser/parsers/mdit.py Normal file
View File

@ -0,0 +1,123 @@
"""This module holds the ``create_md_parser`` function,
which creates a parser from the config.
"""
from __future__ import annotations
from typing import Callable
from markdown_it import MarkdownIt
from markdown_it.renderer import RendererProtocol
from mdit_py_plugins.amsmath import amsmath_plugin
from mdit_py_plugins.anchors import anchors_plugin
from mdit_py_plugins.attrs import attrs_plugin
from mdit_py_plugins.colon_fence import colon_fence_plugin
from mdit_py_plugins.deflist import deflist_plugin
from mdit_py_plugins.dollarmath import dollarmath_plugin
from mdit_py_plugins.field_list import fieldlist_plugin
from mdit_py_plugins.footnote import footnote_plugin
from mdit_py_plugins.front_matter import front_matter_plugin
from mdit_py_plugins.myst_blocks import myst_block_plugin
from mdit_py_plugins.myst_role import myst_role_plugin
from mdit_py_plugins.substitution import substitution_plugin
from mdit_py_plugins.tasklists import tasklists_plugin
from mdit_py_plugins.wordcount import wordcount_plugin
from myst_parser.config.main import MdParserConfig
def create_md_parser(
config: MdParserConfig, renderer: Callable[[MarkdownIt], RendererProtocol]
) -> MarkdownIt:
"""Return a Markdown parser with the required MyST configuration."""
# TODO warn if linkify required and linkify-it-py not installed
# (currently the parse will unceremoniously except)
if config.commonmark_only:
# see https://spec.commonmark.org/
md = MarkdownIt("commonmark", renderer_cls=renderer).use(
wordcount_plugin, per_minute=config.words_per_minute
)
md.options.update({"myst_config": config})
return md
if config.gfm_only:
# see https://github.github.com/gfm/
md = (
MarkdownIt("commonmark", renderer_cls=renderer)
# note, strikethrough currently only supported tentatively for HTML
.enable("strikethrough")
.enable("table")
.use(tasklists_plugin)
.enable("linkify")
.use(wordcount_plugin, per_minute=config.words_per_minute)
)
md.options.update({"linkify": True, "myst_config": config})
return md
md = (
MarkdownIt("commonmark", renderer_cls=renderer)
.enable("table")
.use(front_matter_plugin)
.use(myst_block_plugin)
.use(myst_role_plugin)
.use(footnote_plugin)
.use(wordcount_plugin, per_minute=config.words_per_minute)
.disable("footnote_inline")
# disable this for now, because it need a new implementation in the renderer
.disable("footnote_tail")
)
typographer = False
if "smartquotes" in config.enable_extensions:
md.enable("smartquotes")
typographer = True
if "replacements" in config.enable_extensions:
md.enable("replacements")
typographer = True
if "linkify" in config.enable_extensions:
md.enable("linkify")
if md.linkify is not None:
md.linkify.set({"fuzzy_link": config.linkify_fuzzy_links})
if "strikethrough" in config.enable_extensions:
md.enable("strikethrough")
if "dollarmath" in config.enable_extensions:
md.use(
dollarmath_plugin,
allow_labels=config.dmath_allow_labels,
allow_space=config.dmath_allow_space,
allow_digits=config.dmath_allow_digits,
double_inline=config.dmath_double_inline,
)
if "colon_fence" in config.enable_extensions:
md.use(colon_fence_plugin)
if "amsmath" in config.enable_extensions:
md.use(amsmath_plugin)
if "deflist" in config.enable_extensions:
md.use(deflist_plugin)
if "fieldlist" in config.enable_extensions:
md.use(fieldlist_plugin)
if "tasklist" in config.enable_extensions:
md.use(tasklists_plugin)
if "substitution" in config.enable_extensions:
md.use(substitution_plugin, *config.sub_delimiters)
if "attrs_image" in config.enable_extensions:
md.use(attrs_plugin, after=("image",))
if config.heading_anchors is not None:
md.use(
anchors_plugin,
max_level=config.heading_anchors,
slug_func=config.heading_slug_func,
)
for name in config.disable_syntax:
md.disable(name, True)
md.options.update(
{
"typographer": typographer,
"linkify": "linkify" in config.enable_extensions,
"myst_config": config,
}
)
return md

View File

@ -0,0 +1,440 @@
"""A simple but complete HTML to Abstract Syntax Tree (AST) parser.
The AST can also reproduce the HTML text.
Example::
>> text = '<div class="note"><p>text</p></div>'
>> ast = tokenize_html(text)
>> list(ast.walk(include_self=True))
[Root(''), Tag('div', {'class': 'note'}), Tag('p'), Data('text')]
>> str(ast)
'<div class="note"><p>text</p></div>'
>> str(ast[0][0])
'<p>text</p>'
Note: optional tags are not accounted for
(see https://html.spec.whatwg.org/multipage/syntax.html#optional-tags)
"""
from __future__ import annotations
import inspect
import itertools
from collections import abc, deque
from html.parser import HTMLParser
from typing import Any, Callable, Iterable, Iterator
class Attribute(dict):
"""This class holds the tags's attributes."""
def __getitem__(self, key: str) -> str:
"""If self doesn't have the key it returns ''."""
return self.get(key, "")
@property
def classes(self) -> list[str]:
"""Return 'class' attribute as list."""
return self["class"].split()
def __str__(self) -> str:
"""Return a htmlized representation for attributes."""
return " ".join(f'{key}="{value}"' for key, value in self.items())
class Element(abc.MutableSequence):
"""An Element of the xml/html document.
All xml/html entities inherit from this class.
"""
def __init__(self, name: str = "", attr: dict | None = None) -> None:
"""Initialise the element."""
self.name = name
self.attrs: Attribute = Attribute(attr or {})
self._parent: Element | None = None
self._children: list[Element] = []
@property
def parent(self) -> Element | None:
"""Return parent."""
return self._parent
@property
def children(self) -> list[Element]:
"""Return copy of children."""
return self._children[:]
def reset_children(self, children: list[Element], deepcopy: bool = False):
new_children = []
for i, item in enumerate(children):
assert isinstance(item, Element)
if deepcopy:
item = item.deepcopy()
if item._parent is None:
item._parent = self
elif item._parent != self:
raise AssertionError(f"different parent already set for item {i}")
new_children.append(item)
self._children = new_children
def __getitem__(self, index: int) -> Element: # type: ignore[override]
return self._children[index]
def __setitem__(self, index: int, item: Element): # type: ignore[override]
assert isinstance(item, Element)
if item._parent is not None and item._parent != self:
raise AssertionError(f"different parent already set for: {item!r}")
item._parent = self
return self._children.__setitem__(index, item)
def __delitem__(self, index: int): # type: ignore[override]
return self._children.__delitem__(index)
def __len__(self) -> int:
return self._children.__len__()
def __iter__(self) -> Iterator[Element]:
yield from self._children
def insert(self, index: int, item: Element):
assert isinstance(item, Element)
if item._parent is not None and item._parent != self:
raise AssertionError(f"different parent already set for: {item!r}")
item._parent = self
return self._children.insert(index, item)
def deepcopy(self) -> Element:
"""Recursively copy and remove parent."""
_copy = self.__class__(self.name, self.attrs)
for child in self:
_copy_child = child.deepcopy()
_copy.append(_copy_child)
return _copy
def __repr__(self) -> str:
text = f"{self.__class__.__name__}({self.name!r}"
if self.attrs:
text += f", {self.attrs!r}"
text += ")"
return text
def render(
self,
tag_overrides: dict[str, Callable[[Element, dict], str]] | None = None,
**kwargs,
) -> str:
"""Returns a HTML string representation of the element.
:param tag_overrides: Provide a dictionary of render function
for specific tag names, to override the normal render format
"""
raise NotImplementedError
def __str__(self) -> str:
return self.render()
def __eq__(self, item: Any) -> bool:
return item is self
def walk(self, include_self: bool = False) -> Iterator[Element]:
"""Walk through the xml/html AST."""
if include_self:
yield self
for child in self:
yield child
yield from child.walk()
def strip(self, inplace: bool = False, recurse: bool = False) -> Element:
"""Return copy with all `Data` tokens
that only contain whitespace / newlines removed.
"""
element = self
if not inplace:
element = self.deepcopy()
element.reset_children(
[
e
for e in element.children
if not (isinstance(e, Data) and e.data.strip() == "")
]
)
if recurse:
for child in element:
child.strip(inplace=True, recurse=True)
return element
def find(
self,
identifier: str | type[Element],
attrs: dict | None = None,
classes: Iterable[str] | None = None,
include_self: bool = False,
recurse: bool = True,
) -> Iterator[Element]:
"""Find all elements that match name and specific attributes."""
iterator = self.walk() if recurse else self
if include_self:
iterator = itertools.chain([self], iterator)
if inspect.isclass(identifier):
test_func = lambda c: isinstance(c, identifier) # noqa: E731
else:
test_func = lambda c: c.name == identifier # noqa: E731
classes = set(classes) if classes is not None else classes
for child in iterator:
if test_func(child):
if classes is not None and not classes.issubset(child.attrs.classes):
continue
for key, value in (attrs or {}).items():
if child.attrs[key] != value:
break
else:
yield child
class Root(Element):
"""The root of the AST tree."""
def render(self, **kwargs) -> str: # type: ignore[override]
"""Returns a string HTML representation of the structure."""
return "".join(child.render(**kwargs) for child in self)
class Tag(Element):
"""Represent xml/html tags under the form: <name key="value" ...> ... </name>."""
def render(
self,
tag_overrides: dict[str, Callable[[Element, dict], str]] | None = None,
**kwargs,
) -> str:
if tag_overrides and self.name in tag_overrides:
return tag_overrides[self.name](self, tag_overrides)
return (
f"<{self.name}{' ' if self.attrs else ''}{self.attrs}>"
+ "".join(
child.render(tag_overrides=tag_overrides, **kwargs) for child in self
)
+ f"</{self.name}>"
)
class XTag(Element):
"""Represent XHTML style tags with no children, like `<img src="t.gif" />`"""
def render(
self,
tag_overrides: dict[str, Callable[[Element, dict], str]] | None = None,
**kwargs,
) -> str:
if tag_overrides is not None and self.name in tag_overrides:
return tag_overrides[self.name](self, tag_overrides)
return f"<{self.name}{' ' if self.attrs else ''}{self.attrs}/>"
class VoidTag(Element):
"""Represent tags with no children, only start tag, like `<img src="t.gif" >`"""
def render(self, **kwargs) -> str: # type: ignore[override]
return f"<{self.name}{' ' if self.attrs else ''}{self.attrs}>"
class TerminalElement(Element):
def __init__(self, data: str):
super().__init__("")
self.data: str = data
def __repr__(self) -> str:
text = self.data
if len(text) > 20:
text = text[:17] + "..."
return f"{self.__class__.__name__}({text!r})"
def deepcopy(self) -> TerminalElement:
"""Copy and remove parent."""
_copy = self.__class__(self.data)
return _copy
class Data(TerminalElement):
"""Represent data inside xml/html documents, like raw text."""
def render(self, **kwargs) -> str: # type: ignore[override]
return self.data
class Declaration(TerminalElement):
"""Represent declarations, like `<!DOCTYPE html>`"""
def render(self, **kwargs) -> str: # type: ignore[override]
return f"<!{self.data}>"
class Comment(TerminalElement):
"""Represent HTML comments"""
def render(self, **kwargs) -> str: # type: ignore[override]
return f"<!--{self.data}-->"
class Pi(TerminalElement):
"""Represent processing instructions like `<?xml-stylesheet ?>`"""
def render(self, **kwargs) -> str: # type: ignore[override]
return f"<?{self.data}>"
class Char(TerminalElement):
"""Represent character codes like: `&#0`"""
def render(self, **kwargs) -> str: # type: ignore[override]
return f"&#{self.data};"
class Entity(TerminalElement):
"""Represent entities like `&amp`"""
def render(self, **kwargs) -> str: # type: ignore[override]
return f"&{self.data};"
class Tree:
"""The engine class to generate the AST tree."""
def __init__(self, name: str = ""):
"""Initialise Tree"""
self.name = name
self.outmost = Root(name)
self.stack: deque = deque()
self.stack.append(self.outmost)
def clear(self):
"""Clear the outmost and stack for a new parsing."""
self.outmost = Root(self.name)
self.stack.clear()
self.stack.append(self.outmost)
def last(self) -> Element:
"""Return the last pointer which point to the actual tag scope."""
return self.stack[-1]
def nest_tag(self, name: str, attrs: dict):
"""Nest a given tag at the bottom of the tree using
the last stack's pointer.
"""
pointer = self.stack.pop()
item = Tag(name, attrs)
pointer.append(item)
self.stack.append(pointer)
self.stack.append(item)
def nest_xtag(self, name: str, attrs: dict):
"""Nest an XTag onto the tree."""
top = self.last()
item = XTag(name, attrs)
top.append(item)
def nest_vtag(self, name: str, attrs: dict):
"""Nest a VoidTag onto the tree."""
top = self.last()
item = VoidTag(name, attrs)
top.append(item)
def nest_terminal(self, klass: type[TerminalElement], data: str):
"""Nest the data onto the tree."""
top = self.last()
item = klass(data)
top.append(item)
def enclose(self, name: str):
"""When a closing tag is found, pop the pointer's scope from the stack,
to then point to the earlier scope's tag.
"""
count = 0
for ind in reversed(self.stack):
count = count + 1
if ind.name == name:
break
else:
count = 0
# It pops all the items which do not match with the closing tag.
for _ in range(0, count):
self.stack.pop()
class HtmlToAst(HTMLParser):
"""The tokenizer class."""
# see https://html.spec.whatwg.org/multipage/syntax.html#void-elements
void_elements = {
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
}
def __init__(self, name: str = "", convert_charrefs: bool = False):
super().__init__(convert_charrefs=convert_charrefs)
self.struct = Tree(name)
def feed(self, source: str) -> Root: # type: ignore[override]
"""Parse the source string."""
self.struct.clear()
super().feed(source)
return self.struct.outmost
def handle_starttag(self, name: str, attr):
"""When found an opening tag then nest it onto the tree."""
if name in self.void_elements:
self.struct.nest_vtag(name, attr)
else:
self.struct.nest_tag(name, attr)
def handle_startendtag(self, name: str, attr):
"""When found a XHTML tag style then nest it up to the tree."""
self.struct.nest_xtag(name, attr)
def handle_endtag(self, name: str):
"""When found a closing tag then makes it point to the right scope."""
if name not in self.void_elements:
self.struct.enclose(name)
def handle_data(self, data: str):
"""Nest data onto the tree."""
self.struct.nest_terminal(Data, data)
def handle_decl(self, decl: str):
self.struct.nest_terminal(Declaration, decl)
def unknown_decl(self, decl: str):
self.struct.nest_terminal(Declaration, decl)
def handle_charref(self, data: str):
self.struct.nest_terminal(Char, data)
def handle_entityref(self, data: str):
self.struct.nest_terminal(Entity, data)
def handle_pi(self, data: str):
self.struct.nest_terminal(Pi, data)
def handle_comment(self, data: str):
self.struct.nest_terminal(Comment, data)
def tokenize_html(text: str, name: str = "", convert_charrefs: bool = False) -> Root:
parser = HtmlToAst(name, convert_charrefs=convert_charrefs)
return parser.feed(text)

View File

@ -0,0 +1,69 @@
"""MyST Markdown parser for sphinx."""
from __future__ import annotations
from docutils import nodes
from docutils.parsers.rst import Parser as RstParser
from sphinx.parsers import Parser as SphinxParser
from sphinx.util import logging
from myst_parser.config.main import (
MdParserConfig,
TopmatterReadError,
merge_file_level,
read_topmatter,
)
from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer, create_warning
from myst_parser.parsers.mdit import create_md_parser
SPHINX_LOGGER = logging.getLogger(__name__)
class MystParser(SphinxParser):
"""Sphinx parser for Markedly Structured Text (MyST)."""
supported: tuple[str, ...] = ("md", "markdown", "myst")
"""Aliases this parser supports."""
settings_spec = RstParser.settings_spec
"""Runtime settings specification.
Defines runtime settings and associated command-line options, as used by
`docutils.frontend.OptionParser`. This is a concatenation of tuples of:
- Option group title (string or `None` which implies no group, just a list
of single options).
- Description (string or `None`).
- A sequence of option tuples
"""
config_section = "myst parser"
config_section_dependencies = ("parsers",)
translate_section_name = None
def parse(self, inputstring: str, document: nodes.document) -> None:
"""Parse source text.
:param inputstring: The source string to parse
:param document: The root docutils node to add AST elements to
"""
# get the global config
config: MdParserConfig = document.settings.env.myst_config
# update the global config with the file-level config
try:
topmatter = read_topmatter(inputstring)
except TopmatterReadError:
pass # this will be reported during the render
else:
if topmatter:
warning = lambda wtype, msg: create_warning( # noqa: E731
document, msg, line=1, append_to=document, subtype=wtype
)
config = merge_file_level(config, topmatter, warning)
parser = create_md_parser(config, SphinxRenderer)
parser.options["document"] = document
parser.render(inputstring)

1
myst_parser/py.typed Normal file
View File

@ -0,0 +1 @@
# Marker file for PEP 561

6
myst_parser/sphinx_.py Normal file
View File

@ -0,0 +1,6 @@
"""A module for compatibility with the docutils>=0.17 `include` directive, in RST documents::
.. include:: path/to/file.md
:parser: myst_parser.sphinx_
"""
from myst_parser.parsers.sphinx_ import MystParser as Parser # noqa: F401

View File

@ -0,0 +1 @@
"""Sphinx extension for myst_parser."""

View File

@ -0,0 +1,136 @@
"""MyST specific directives"""
from copy import copy
from typing import List, Tuple, cast
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.directives import SphinxDirective
from sphinx.util.docutils import SphinxRole
from myst_parser.mocking import MockState
def align(argument):
return directives.choice(argument, ("left", "center", "right"))
def figwidth_value(argument):
if argument.lower() == "image":
return "image"
else:
return directives.length_or_percentage_or_unitless(argument, "px")
class SubstitutionReferenceRole(SphinxRole):
"""Implement substitution references as a role.
Note, in ``docutils/parsers/rst/roles.py`` this is left unimplemented.
"""
def run(self) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
subref_node = nodes.substitution_reference(self.rawtext, self.text)
self.set_source_info(subref_node, self.lineno)
subref_node["refname"] = nodes.fully_normalize_name(self.text)
return [subref_node], []
class FigureMarkdown(SphinxDirective):
"""Directive for creating a figure with Markdown compatible syntax.
Example::
:::{figure-md} target
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="200px">
This is a caption in **Markdown**
:::
"""
required_arguments = 0
optional_arguments = 1 # image target
final_argument_whitespace = True
has_content = True
option_spec = {
"width": figwidth_value,
"class": directives.class_option,
"align": align,
"name": directives.unchanged,
}
def run(self) -> List[nodes.Node]:
figwidth = self.options.pop("width", None)
figclasses = self.options.pop("class", None)
align = self.options.pop("align", None)
if not isinstance(self.state, MockState):
return [self.figure_error("Directive is only supported in myst parser")]
state = cast(MockState, self.state)
# ensure html image enabled
myst_extensions = copy(state._renderer.md_config.enable_extensions)
node = nodes.Element()
try:
state._renderer.md_config.enable_extensions = list(
state._renderer.md_config.enable_extensions
) + ["html_image"]
state.nested_parse(self.content, self.content_offset, node)
finally:
state._renderer.md_config.enable_extensions = myst_extensions
if not len(node.children) == 2:
return [
self.figure_error(
"content should be one image, "
"followed by a single paragraph caption"
)
]
image_node, caption_para = node.children
if isinstance(image_node, nodes.paragraph):
image_node = image_node[0]
if not isinstance(image_node, nodes.image):
return [
self.figure_error(
"content should be one image (not found), "
"followed by single paragraph caption"
)
]
if not isinstance(caption_para, nodes.paragraph):
return [
self.figure_error(
"content should be one image, "
"followed by single paragraph caption (not found)"
)
]
caption_node = nodes.caption(caption_para.rawsource, "", *caption_para.children)
caption_node.source = caption_para.source
caption_node.line = caption_para.line
figure_node = nodes.figure("", image_node, caption_node)
self.set_source_info(figure_node)
if figwidth is not None:
figure_node["width"] = figwidth
if figclasses:
figure_node["classes"] += figclasses
if align:
figure_node["align"] = align
if self.arguments:
self.options["name"] = self.arguments[0]
self.add_name(figure_node)
return [figure_node]
def figure_error(self, message):
"""A warning for reporting an invalid figure."""
error = self.state_machine.reporter.error(
message,
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno,
)
return error

View File

@ -0,0 +1,60 @@
"""The setup for the sphinx extension."""
from typing import Any
from sphinx.application import Sphinx
def setup_sphinx(app: Sphinx, load_parser=False):
"""Initialize all settings and transforms in Sphinx."""
# we do this separately to setup,
# so that it can be called by external packages like myst_nb
from myst_parser.config.main import MdParserConfig
from myst_parser.parsers.sphinx_ import MystParser
from myst_parser.sphinx_ext.directives import (
FigureMarkdown,
SubstitutionReferenceRole,
)
from myst_parser.sphinx_ext.mathjax import override_mathjax
from myst_parser.sphinx_ext.myst_refs import MystReferenceResolver
if load_parser:
app.add_source_suffix(".md", "markdown")
app.add_source_parser(MystParser)
app.add_role("sub-ref", SubstitutionReferenceRole())
app.add_directive("figure-md", FigureMarkdown)
app.add_post_transform(MystReferenceResolver)
for name, default, field in MdParserConfig().as_triple():
if not field.metadata.get("docutils_only", False):
# TODO add types?
app.add_config_value(f"myst_{name}", default, "env", types=Any)
app.connect("builder-inited", create_myst_config)
app.connect("builder-inited", override_mathjax)
def create_myst_config(app):
from sphinx.util import logging
# Ignore type checkers because the attribute is dynamically assigned
from sphinx.util.console import bold # type: ignore[attr-defined]
from myst_parser import __version__
from myst_parser.config.main import MdParserConfig
logger = logging.getLogger(__name__)
values = {
name: app.config[f"myst_{name}"]
for name, _, field in MdParserConfig().as_triple()
if not field.metadata.get("docutils_only", False)
}
try:
app.env.myst_config = MdParserConfig(**values)
logger.info(bold("myst v%s:") + " %s", __version__, app.env.myst_config)
except (TypeError, ValueError) as error:
logger.error("myst configuration invalid: %s", error.args[0])
app.env.myst_config = MdParserConfig()

View File

@ -0,0 +1,118 @@
"""Overrides to ``sphinx.ext.mathjax``
This fixes two issues:
1. Mathjax should not search for ``$`` delimiters, nor LaTeX amsmath environments,
since we already achieve this with the dollarmath and amsmath mrakdown-it-py plugins
2. amsmath math blocks should be wrapped in mathjax delimiters (default ``\\[...\\]``),
and assigned an equation number
"""
from docutils import nodes
from sphinx.application import Sphinx
from sphinx.ext import mathjax
from sphinx.locale import _
from sphinx.util import logging
from sphinx.util.math import get_node_equation_number
from sphinx.writers.html import HTMLTranslator
logger = logging.getLogger(__name__)
def log_override_warning(app: Sphinx, version: int, current: str, new: str) -> None:
"""Log a warning if MathJax configuration being overridden."""
if logging.is_suppressed_warning("myst", "mathjax", app.config.suppress_warnings):
return
config_name = (
"mathjax3_config['options']['processHtmlClass']"
if version == 3
else "mathjax_config['tex2jax']['processClass']"
)
logger.warning(
f"`{config_name}` is being overridden by myst-parser: '{current}' -> '{new}'. "
"Set `suppress_warnings=['myst.mathjax']` to ignore this warning, or "
"`myst_update_mathjax=False` if this is undesirable."
)
def override_mathjax(app: Sphinx):
"""Override aspects of the mathjax extension.
MyST-Parser parses dollar and latex math, via markdown-it plugins.
Therefore, we tell Mathjax to only render these HTML elements.
This is accompanied by setting the `ignoreClass` on the top-level section of each MyST document.
"""
if (
"amsmath" in app.config["myst_enable_extensions"]
and "mathjax" in app.registry.html_block_math_renderers
):
app.registry.html_block_math_renderers["mathjax"] = (
html_visit_displaymath, # type: ignore[assignment]
None,
)
if "dollarmath" not in app.config["myst_enable_extensions"]:
return
if not app.env.myst_config.update_mathjax: # type: ignore
return
mjax_classes = app.env.myst_config.mathjax_classes # type: ignore
if "mathjax3_config" in app.config:
# sphinx 4 + mathjax 3
app.config.mathjax3_config = app.config.mathjax3_config or {} # type: ignore
app.config.mathjax3_config.setdefault("options", {})
if (
"processHtmlClass" in app.config.mathjax3_config["options"]
and app.config.mathjax3_config["options"]["processHtmlClass"]
!= mjax_classes
):
log_override_warning(
app,
3,
app.config.mathjax3_config["options"]["processHtmlClass"],
mjax_classes,
)
app.config.mathjax3_config["options"]["processHtmlClass"] = mjax_classes
elif "mathjax_config" in app.config:
# sphinx 3 + mathjax 2
app.config.mathjax_config = app.config.mathjax_config or {} # type: ignore[attr-defined]
app.config.mathjax_config.setdefault("tex2jax", {})
if (
"processClass" in app.config.mathjax_config["tex2jax"]
and app.config.mathjax_config["tex2jax"]["processClass"] != mjax_classes
):
log_override_warning(
app,
2,
app.config.mathjax_config["tex2jax"]["processClass"],
mjax_classes,
)
app.config.mathjax_config["tex2jax"]["processClass"] = mjax_classes
def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None:
"""Override for sphinx.ext.mathjax.html_visit_displaymath to handle amsmath.
By default displaymath, are normally wrapped in a prefix/suffix,
defined by mathjax_display, and labelled nodes are numbered.
However, this is not the case if the math_block is set as 'nowrap', as for amsmath.
Therefore, we need to override this behaviour.
"""
if "amsmath" in node.get("classes", []):
self.body.append(
self.starttag(node, "div", CLASS="math notranslate nohighlight amsmath")
)
if node["number"]:
number = get_node_equation_number(self, node)
self.body.append('<span class="eqno">(%s)' % number)
self.add_permalink_ref(node, _("Permalink to this equation"))
self.body.append("</span>")
prefix, suffix = self.builder.config.mathjax_display
self.body.append(prefix)
self.body.append(self.encode(node.astext()))
self.body.append(suffix)
self.body.append("</div>\n")
raise nodes.SkipNode
return mathjax.html_visit_displaymath(self, node)

View File

@ -0,0 +1,282 @@
"""A post-transform for overriding the behaviour of sphinx reference resolution.
This is applied to MyST type references only, such as ``[text](target)``,
and allows for nested syntax
"""
import os
from typing import Any, List, Optional, Tuple, cast
from docutils import nodes
from docutils.nodes import Element, document
from sphinx import addnodes, version_info
from sphinx.addnodes import pending_xref
from sphinx.domains.std import StandardDomain
from sphinx.locale import __
from sphinx.transforms.post_transforms import ReferencesResolver
from sphinx.util import docname_join, logging
from sphinx.util.nodes import clean_astext, make_refnode
from myst_parser._compat import findall
try:
from sphinx.errors import NoUri
except ImportError:
# sphinx < 2.1
from sphinx.environment import NoUri # type: ignore
logger = logging.getLogger(__name__)
class MystReferenceResolver(ReferencesResolver):
"""Resolves cross-references on doctrees.
Overrides default sphinx implementation, to allow for nested syntax
"""
default_priority = 9 # higher priority than ReferencesResolver (10)
def run(self, **kwargs: Any) -> None:
self.document: document
for node in findall(self.document)(addnodes.pending_xref):
if node["reftype"] != "myst":
continue
contnode = cast(nodes.TextElement, node[0].deepcopy())
newnode = None
target = node["reftarget"]
refdoc = node.get("refdoc", self.env.docname)
domain = None
try:
newnode = self.resolve_myst_ref(refdoc, node, contnode)
if newnode is None:
# no new node found? try the missing-reference event
# but first we change the the reftype to 'any'
# this means it is picked up by extensions like intersphinx
node["reftype"] = "any"
try:
newnode = self.app.emit_firstresult(
"missing-reference",
self.env,
node,
contnode,
**(
{"allowed_exceptions": (NoUri,)}
if version_info[0] > 2
else {}
),
)
finally:
node["reftype"] = "myst"
# still not found? warn if node wishes to be warned about or
# we are in nit-picky mode
if newnode is None:
node["refdomain"] = ""
# TODO ideally we would override the warning message here,
# to show the [ref.myst] for suppressing warning
self.warn_missing_reference(
refdoc, node["reftype"], target, node, domain
)
except NoUri:
newnode = contnode
node.replace_self(newnode or contnode)
def resolve_myst_ref(
self, refdoc: str, node: pending_xref, contnode: Element
) -> Element:
"""Resolve reference generated by the "myst" role; ``[text](reference)``.
This builds on the sphinx ``any`` role to also resolve:
- Document references with extensions; ``[text](./doc.md)``
- Document references with anchors with anchors; ``[text](./doc.md#target)``
- Nested syntax for explicit text with std:doc and std:ref;
``[**nested**](reference)``
"""
target = node["reftarget"] # type: str
results = [] # type: List[Tuple[str, Element]]
res_anchor = self._resolve_anchor(node, refdoc)
if res_anchor:
results.append(("std:doc", res_anchor))
else:
# if we've already found an anchored doc,
# don't search in the std:ref/std:doc (leads to duplication)
# resolve standard references
res = self._resolve_ref_nested(node, refdoc)
if res:
results.append(("std:ref", res))
# resolve doc names
res = self._resolve_doc_nested(node, refdoc)
if res:
results.append(("std:doc", res))
# get allowed domains for referencing
ref_domains = self.env.config.myst_ref_domains
assert self.app.builder
# next resolve for any other standard reference objects
if ref_domains is None or "std" in ref_domains:
stddomain = cast(StandardDomain, self.env.get_domain("std"))
for objtype in stddomain.object_types:
key = (objtype, target)
if objtype == "term":
key = (objtype, target.lower())
if key in stddomain.objects:
docname, labelid = stddomain.objects[key]
domain_role = "std:" + stddomain.role_for_objtype(objtype)
ref_node = make_refnode(
self.app.builder, refdoc, docname, labelid, contnode
)
results.append((domain_role, ref_node))
# finally resolve for any other type of allowed reference domain
for domain in self.env.domains.values():
if domain.name == "std":
continue # we did this one already
if ref_domains is not None and domain.name not in ref_domains:
continue
try:
results.extend(
domain.resolve_any_xref(
self.env, refdoc, self.app.builder, target, node, contnode
)
)
except NotImplementedError:
# the domain doesn't yet support the new interface
# we have to manually collect possible references (SLOW)
if not (getattr(domain, "__module__", "").startswith("sphinx.")):
logger.warning(
f"Domain '{domain.__module__}::{domain.name}' has not "
"implemented a `resolve_any_xref` method [myst.domains]",
type="myst",
subtype="domains",
once=True,
)
for role in domain.roles:
res = domain.resolve_xref(
self.env, refdoc, self.app.builder, role, target, node, contnode
)
if res and len(res) and isinstance(res[0], nodes.Element):
results.append((f"{domain.name}:{role}", res))
# now, see how many matches we got...
if not results:
return None
if len(results) > 1:
def stringify(name, node):
reftitle = node.get("reftitle", node.astext())
return f":{name}:`{reftitle}`"
candidates = " or ".join(stringify(name, role) for name, role in results)
logger.warning(
__(
f"more than one target found for 'myst' cross-reference {target}: "
f"could be {candidates} [myst.ref]"
),
location=node,
type="myst",
subtype="ref",
)
res_role, newnode = results[0]
# Override "myst" class with the actual role type to get the styling
# approximately correct.
res_domain = res_role.split(":")[0]
if len(newnode) > 0 and isinstance(newnode[0], nodes.Element):
newnode[0]["classes"] = newnode[0].get("classes", []) + [
res_domain,
res_role.replace(":", "-"),
]
return newnode
def _resolve_anchor(
self, node: pending_xref, fromdocname: str
) -> Optional[Element]:
"""Resolve doc with anchor."""
if self.env.config.myst_heading_anchors is None:
# no target anchors will have been created, so we don't look for them
return None
target = node["reftarget"] # type: str
if "#" not in target:
return None
# the link may be a heading anchor; we need to first get the relative path
rel_path, anchor = target.rsplit("#", 1)
rel_path = os.path.normpath(rel_path)
if rel_path == ".":
# anchor in the same doc as the node
doc_path = self.env.doc2path(node.get("refdoc", fromdocname), base=False)
else:
# anchor in a different doc from the node
doc_path = os.path.normpath(
os.path.join(node.get("refdoc", fromdocname), "..", rel_path)
)
return self._resolve_ref_nested(node, fromdocname, doc_path + "#" + anchor)
def _resolve_ref_nested(
self, node: pending_xref, fromdocname: str, target=None
) -> Optional[Element]:
"""This is the same as ``sphinx.domains.std._resolve_ref_xref``,
but allows for nested syntax, rather than converting the inner node to raw text.
"""
stddomain = cast(StandardDomain, self.env.get_domain("std"))
target = target or node["reftarget"].lower()
if node["refexplicit"]:
# reference to anonymous label; the reference uses
# the supplied link caption
docname, labelid = stddomain.anonlabels.get(target, ("", ""))
sectname = node.astext()
innernode = nodes.inline(sectname, "")
innernode.extend(node[0].children)
else:
# reference to named label; the final node will
# contain the section name after the label
docname, labelid, sectname = stddomain.labels.get(target, ("", "", ""))
innernode = nodes.inline(sectname, sectname)
if not docname:
return None
assert self.app.builder
return make_refnode(self.app.builder, fromdocname, docname, labelid, innernode)
def _resolve_doc_nested(
self, node: pending_xref, fromdocname: str
) -> Optional[Element]:
"""This is the same as ``sphinx.domains.std._resolve_doc_xref``,
but allows for nested syntax, rather than converting the inner node to raw text.
It also allows for extensions on document names.
"""
# directly reference to document by source name; can be absolute or relative
refdoc = node.get("refdoc", fromdocname)
docname = docname_join(refdoc, node["reftarget"])
if docname not in self.env.all_docs:
# try stripping known extensions from doc name
if os.path.splitext(docname)[1] in self.env.config.source_suffix:
docname = os.path.splitext(docname)[0]
if docname not in self.env.all_docs:
return None
if node["refexplicit"]:
# reference with explicit title
caption = node.astext()
innernode = nodes.inline(caption, "", classes=["doc"])
innernode.extend(node[0].children)
else:
# TODO do we want nested syntax for titles?
caption = clean_astext(self.env.titles[docname])
innernode = nodes.inline(caption, caption, classes=["doc"])
assert self.app.builder
return make_refnode(self.app.builder, fromdocname, docname, "", innernode)

110
pyproject.toml Normal file
View File

@ -0,0 +1,110 @@
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "myst-parser"
dynamic = ["version", "description"]
authors = [{name = "Chris Sewell", email = "chrisj_sewell@hotmail.com"}]
readme = "README.md"
license = {file = "LICENSE"}
classifiers = [
"Development Status :: 4 - Beta",
"Framework :: Sphinx :: Extension",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Text Processing :: Markup",
]
keywords = [
"markdown",
"lexer",
"parser",
"development",
"docutils",
"sphinx",
]
requires-python = ">=3.7"
dependencies = [
"docutils>=0.15,<0.20",
"jinja2", # required for substitutions, but let sphinx choose version
"markdown-it-py>=1.0.0,<3.0.0",
"mdit-py-plugins~=0.3.1",
"pyyaml",
"sphinx>=4,<6",
"typing-extensions",
]
[project.urls]
Homepage = "https://github.com/executablebooks/MyST-Parser"
Documentation = "https://myst-parser.readthedocs.io"
[project.optional-dependencies]
code_style = ["pre-commit~=2.12"]
# for use with "linkify" extension
linkify = ["linkify-it-py~=1.0"]
# Note: This is only required for internal use
rtd = [
"ipython",
"sphinx-book-theme",
"sphinx-design",
"sphinxext-rediraffe~=0.2.7",
"sphinxcontrib.mermaid~=0.7.1",
"sphinxext-opengraph~=0.6.3",
]
testing = [
"beautifulsoup4",
"coverage[toml]",
"pytest>=6,<7",
"pytest-cov",
"pytest-regressions",
"pytest-param-files~=0.3.4",
"sphinx-pytest",
"sphinx<5.2", # TODO 5.2 changes the attributes of desc/desc_signature nodes
]
[project.scripts]
myst-anchors = "myst_parser.cli:print_anchors"
myst-docutils-html = "myst_parser.parsers.docutils_:cli_html"
myst-docutils-html5 = "myst_parser.parsers.docutils_:cli_html5"
myst-docutils-latex = "myst_parser.parsers.docutils_:cli_latex"
myst-docutils-xml = "myst_parser.parsers.docutils_:cli_xml"
myst-docutils-pseudoxml = "myst_parser.parsers.docutils_:cli_pseudoxml"
[tool.flit.module]
name = "myst_parser"
[tool.flit.sdist]
exclude = [
"docs/",
"tests/",
]
[tool.isort]
profile = "black"
known_first_party = ["myst_parser", "tests"]
known_third_party = ["docutils", "markdown_it", "sphinx"]
# Group first party and local folder imports together
no_lines_before = "LOCALFOLDER"
[tool.mypy]
show_error_codes = true
check_untyped_defs = true
strict_equality = true
no_implicit_optional = true
warn_unused_ignores = true
[[tool.mypy.overrides]]
module = ["docutils.*", "yaml.*"]
ignore_missing_imports = true
[tool.coverage.run]
omit = ["*/_docs.py"]

15
tests/test_cli.py Normal file
View File

@ -0,0 +1,15 @@
from unittest import mock
from myst_parser.cli import print_anchors
def test_print_anchors():
from io import StringIO
in_stream = StringIO("# a\n\n## b\n\ntext")
out_stream = StringIO()
with mock.patch("sys.stdin", in_stream):
with mock.patch("sys.stdout", out_stream):
print_anchors(["-l", "1"])
out_stream.seek(0)
assert out_stream.read().strip() == '<h1 id="a"></h1>'

File diff suppressed because it is too large Load Diff

26
tests/test_commonmark/spec.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -e
REPO="https://github.com/commonmark/CommonMark.git"
VERSION="0.29"
function main {
echo "Cloning from repo: $REPO..."
git clone --quiet $REPO
echo "Using version $VERSION..."
cd "CommonMark"
git checkout --quiet $VERSION
echo "Dumping tests file..."
python3 "test/spec_tests.py" --dump-tests > "../commonmark.json"
echo "Cleaning up..."
cd ..
rm -rf CommonMark
echo "Done."
}
main

View File

@ -0,0 +1,44 @@
"""In this module tests are run against the full test set,
provided by https://github.com/commonmark/CommonMark.git.
"""
import json
import os
import pytest
from markdown_it.renderer import RendererHTML
from myst_parser.config.main import MdParserConfig
from myst_parser.parsers.mdit import create_md_parser
with open(
os.path.join(os.path.dirname(__file__), "commonmark.json"), encoding="utf8"
) as fin:
tests = json.load(fin)
@pytest.mark.parametrize("entry", tests)
def test_commonmark(entry):
if entry["example"] == 14:
# This is just a test that +++ are not parsed as thematic breaks
pytest.skip("Expects '+++' to be unconverted (not block break).")
if entry["example"] in [66, 68]:
# Front matter is supported by numerous Markdown flavours,
# but not strictly CommonMark,
# see: https://talk.commonmark.org/t/metadata-in-documents/721/86
pytest.skip(
"Thematic breaks on the first line conflict with front matter syntax"
)
test_case = entry["markdown"]
md = create_md_parser(MdParserConfig(), RendererHTML)
output = md.render(test_case)
if entry["example"] == 593:
# this doesn't have any bearing on the output
output = output.replace("mailto", "MAILTO")
if entry["example"] in [187, 209, 210]:
# this doesn't have any bearing on the output
output = output.replace(
"<blockquote></blockquote>", "<blockquote>\n</blockquote>"
)
assert output == entry["html"]

116
tests/test_docutils.py Normal file
View File

@ -0,0 +1,116 @@
import io
from dataclasses import dataclass, field, fields
from textwrap import dedent
import pytest
from docutils import VersionInfo, __version_info__
from typing_extensions import Literal
from myst_parser.mdit_to_docutils.base import make_document
from myst_parser.parsers.docutils_ import (
Parser,
attr_to_optparse_option,
cli_html,
cli_html5,
cli_latex,
cli_pseudoxml,
cli_xml,
)
def test_attr_to_optparse_option():
@dataclass
class Config:
name: Literal["a"] = field(default="default")
output = attr_to_optparse_option(fields(Config)[0], "default")
assert len(output) == 3
def test_parser():
"""Test calling `Parser.parse` directly."""
parser = Parser()
document = make_document(parser_cls=Parser)
parser.parse("something", document)
assert (
document.pformat().strip()
== '<document source="notset">\n <paragraph>\n something'
)
def test_cli_html(monkeypatch, capsys):
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"text")))
cli_html([])
captured = capsys.readouterr()
assert not captured.err
assert "text" in captured.out
def test_cli_html5(monkeypatch, capsys):
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"text")))
cli_html5([])
captured = capsys.readouterr()
assert not captured.err
assert "text" in captured.out
def test_cli_latex(monkeypatch, capsys):
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"text")))
cli_latex([])
captured = capsys.readouterr()
assert not captured.err
assert "text" in captured.out
def test_cli_xml(monkeypatch, capsys):
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"text")))
cli_xml([])
captured = capsys.readouterr()
assert not captured.err
assert "text" in captured.out
def test_cli_pseudoxml(monkeypatch, capsys):
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"text")))
cli_pseudoxml([])
captured = capsys.readouterr()
assert not captured.err
assert "text" in captured.out
def test_help_text():
"""Test retrieving settings help text."""
from docutils.frontend import OptionParser
stream = io.StringIO()
OptionParser(components=(Parser,)).print_help(stream)
assert "MyST options" in stream.getvalue()
@pytest.mark.skipif(
__version_info__ < VersionInfo(0, 17, 0, "final", 0, True),
reason="parser option added in docutils 0.17",
)
def test_include_from_rst(tmp_path):
"""Test including a MyST file from within an RST file."""
from docutils.parsers.rst import Parser as RSTParser
include_path = tmp_path.joinpath("include.md")
include_path.write_text("# Title")
parser = RSTParser()
document = make_document(parser_cls=RSTParser)
parser.parse(
f".. include:: {include_path}\n :parser: myst_parser.docutils_", document
)
assert (
document.pformat().strip()
== dedent(
"""\
<document source="notset">
<section ids="title" names="title">
<title>
Title
"""
).strip()
)

124
tests/test_html/html_ast.md Normal file
View File

@ -0,0 +1,124 @@
tags
.
<html>
<head>
<title class="a b" other="x">Title of the document</title>
</head>
<body>
The content of the document......
</body>
</html>
.
Root('')
Tag('html')
Data('\n')
Tag('head')
Data('\n')
Tag('title', {'class': 'a b', 'other': 'x'})
Data('Title of the docu...')
Data('\n')
Data('\n')
Tag('body')
Data('\nThe content of t...')
Data('\n')
Data('\n')
.
un-closed tags
.
<div class="a">
<div class="b">
.
Root('')
Tag('div', {'class': 'a'})
Data('\n')
Tag('div', {'class': 'b'})
Data('\n')
.
xtag
.
<img src="img_girl.jpg" alt="Girl in a jacket" width="500" height="600"/>
.
Root('')
XTag('img', {'src': 'img_girl.jpg', 'alt': 'Girl in a jacket', 'width': '500', 'height': '600'})
Data('\n')
.
data
.
a
.
Root('')
Data('a\n')
.
declaration
.
<!DOCTYPE html>
.
Root('')
Declaration('DOCTYPE html')
Data('\n')
.
process information
.
<?xml-stylesheet ?>
.
Root('')
Pi('xml-stylesheet ?')
Data('\n')
.
entities
.
&amp;
&#123;
.
Root('')
Entity('amp')
Data('\n\n')
Char('123')
Data('\n')
.
comments
.
<!--This is a comment. Comments are not displayed in the browser
-->
.
Root('')
Comment('This is a comment...')
Data('\n')
.
admonition
.
<div class="admonition tip alert alert-warning">
<div class="admonition-title" style="font-weight: bold;">Tip</div>
parameter allows to get a deterministic results even if we
use some random process (i.e. data shuffling).
</div>
.
Root('')
Tag('div', {'class': 'admonition tip alert alert-warning'})
Data('\n')
Tag('div', {'class': 'admonition-title', 'style': 'font-weight: bold;'})
Data('Tip')
Data('\nparameter allows...')
Data('\n')
.
image
.
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="200px">
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="300px">
.
Root('')
VoidTag('img', {'src': 'img/fun-fish.png', 'alt': 'fishy', 'class': 'bg-primary mb-1', 'width': '200px'})
Data('\n')
VoidTag('img', {'src': 'img/fun-fish.png', 'alt': 'fishy', 'class': 'bg-primary mb-1', 'width': '300px'})
Data('\n')
.

View File

@ -0,0 +1,87 @@
tags
.
<html>
<head>
<title class="a b" other="x">Title of the document</title>
</head>
<body>
The content of the document......
</body>
</html>
.
<html>
<head>
<title class="a b" other="x">Title of the document</title>
</head>
<body>
The content of the document......
</body>
</html>
.
un-closed tags
.
<div class="a">
<div class="b">
.
<div class="a">
<div class="b">
</div></div>
.
xtag
.
<img src="img_girl.jpg" alt="Girl in a jacket" width="500" height="600"/>
.
<img src="img_girl.jpg" alt="Girl in a jacket" width="500" height="600"/>
.
data
.
a
.
a
.
declaration
.
<!DOCTYPE html>
.
<!DOCTYPE html>
.
process information
.
<?xml-stylesheet ?>
.
<?xml-stylesheet ?>
.
entities
.
&amp;
&#123;
.
&amp;
&#123;
.
comments
.
<!--This is a comment. Comments are not displayed in the browser
-->
.
<!--This is a comment. Comments are not displayed in the browser
-->
.
image
.
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="200px">
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="300px">
.
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="200px">
<img src="img/fun-fish.png" alt="fishy" class="bg-primary mb-1" width="300px">
.

View File

@ -0,0 +1,199 @@
empty
.
.
<container>
<raw format="html" xml:space="preserve">
.
text
.
abc
.
<container>
<raw format="html" xml:space="preserve">
abc
.
normal HTML
.
<div></div>
.
<container>
<raw format="html" xml:space="preserve">
<div></div>
.
image no src
.
<img>
.
<container>
<system_message>
<paragraph>
error
.
image
.
<img src="a">
.
<container>
<Element first="a" name="image" position="0">
.
image unknown attribute
.
<img src="a" other="b">
.
<container>
<Element first="a" name="image" position="0">
.
image known attributes
.
<img src="a" height="200px" class="a b" name="b" align="left">
.
<container>
<Element first="a" name="image" position="0">
:align: left
:class: a b
:height: 200px
:name: b
.
multiple images
.
<img src="a">
<img src="b">
.
<container>
<Element first="a" name="image" position="0">
<Element first="b" name="image" position="0">
.
admonition no close
.
<div class="admonition">
.
<container>
<Element first="Note" name="admonition" position="0">
:class: admonition
.
admonition
.
<div class="admonition">
</div>
.
<container>
<Element first="Note" name="admonition" position="0">
:class: admonition
.
admonition attributes
.
<div class="admonition tip" name="aname">
</div>
.
<container>
<Element first="Note" name="admonition" position="0">
:class: admonition tip
:name: aname
.
admonition div-title
.
<div class="admonition tip">
<div class="title">*Hallo*</div>
.
<container>
<Element first="*Hallo*" name="admonition" position="0">
:class: admonition tip
.
admonition p-title
.
<div class="admonition tip">
<p class="title">*Hallo*</p>
.
<container>
<Element first="*Hallo*" name="admonition" position="0">
:class: admonition tip
.
admonition title+content
.
<div class="admonition">
<div class="title">*Hallo*</div>
content
</div>
.
<container>
<Element first="*Hallo*" name="admonition" position="0">
:class: admonition
content
.
admonition multiple
.
<div class="admonition">
<div class="title">first</div>
content 1
</div>
<div class="admonition">
<div class="title">second</div>
content 2
</div>
.
<container>
<Element first="first" name="admonition" position="0">
:class: admonition
content 1
<Element first="second" name="admonition" position="0">
:class: admonition
content 2
.
admonition with paragraphs
.
<div class="admonition">
<p>paragraph 1</p>
<p>paragraph 2</p>
</div>
.
<container>
<Element first="Note" name="admonition" position="0">
:class: admonition
paragraph 1
paragraph 2
.
nested
.
<div class="admonition">
<p>Some **content**</p>
<div class="admonition tip">
<div class="title">A *title*</div>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
</div>
.
<container>
<Element first="Note" name="admonition" position="0">
:class: admonition
Some **content**
<div class="admonition tip">
<div class="title">A *title*</div>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
.

View File

@ -0,0 +1,35 @@
from pathlib import Path
from unittest.mock import Mock
import pytest
from docutils import nodes
from myst_parser.config.main import MdParserConfig
from myst_parser.mdit_to_docutils.html_to_nodes import html_to_nodes
FIXTURE_PATH = Path(__file__).parent
@pytest.fixture()
def mock_renderer():
def _run_directive(name: str, first_line: str, content: str, position: int):
node = nodes.Element(name=name, first=first_line, position=position)
node += nodes.Text(content)
return [node]
return Mock(
md_config=MdParserConfig(enable_extensions=["html_image", "html_admonition"]),
document={"source": "source"},
reporter=Mock(
warning=Mock(return_value=nodes.system_message("warning")),
error=Mock(return_value=nodes.system_message("error")),
),
run_directive=_run_directive,
)
@pytest.mark.param_file(FIXTURE_PATH / "html_to_nodes.md")
def test_html_to_nodes(file_params, mock_renderer):
output = nodes.container()
output += html_to_nodes(file_params.content, line_number=0, renderer=mock_renderer)
file_params.assert_expected(output.pformat(), rstrip=True)

View File

@ -0,0 +1,41 @@
from pathlib import Path
import pytest
from myst_parser.parsers.parse_html import tokenize_html
FIXTURE_PATH = Path(__file__).parent
@pytest.mark.param_file(FIXTURE_PATH / "html_ast.md")
def test_html_ast(file_params):
tokens = "\n".join(
repr(t) for t in tokenize_html(file_params.content).walk(include_self=True)
)
file_params.assert_expected(tokens, rstrip=True)
@pytest.mark.param_file(FIXTURE_PATH / "html_round_trip.md")
def test_html_round_trip(file_params):
ast = tokenize_html(file_params.content)
file_params.assert_expected(str(ast), rstrip=True)
def test_render_overrides():
text = "<div><abc></abc></div>"
ast = tokenize_html(text)
def _render_abc(element, *args, **kwargs):
return "hallo"
output = ast.render(tag_overrides={"abc": _render_abc})
assert output == "<div>hallo</div>"
def test_ast_find():
text = (
'<div class="a"><div class="c"><x/><y>z</y><div class="a b"></div></div></div>'
)
ast = tokenize_html(text)
found = list(ast.find("div", classes=["a"]))
assert [e.attrs.classes for e in found] == [["a"], ["a", "b"]]

View File

@ -0,0 +1,52 @@
Single Line:
.
\begin{equation} a \end{equation}
.
<document source="<src>/index.md">
<target ids="equation-mock-uuid">
<math_block classes="amsmath" docname="index" label="mock-uuid" nowrap="True" number="1" xml:space="preserve">
\begin{equation} a \end{equation}
.
Multi Line:
.
\begin{equation}
a
\end{equation}
.
<document source="<src>/index.md">
<target ids="equation-mock-uuid">
<math_block classes="amsmath" docname="index" label="mock-uuid" nowrap="True" number="1" xml:space="preserve">
\begin{equation}
a
\end{equation}
.
Multi Line no number:
.
\begin{equation*}
a
\end{equation*}
.
<document source="<src>/index.md">
<math_block classes="amsmath" nowrap="True" number="True" xml:space="preserve">
\begin{equation*}
a
\end{equation*}
.
In list:
.
- \begin{equation}
a = 1
\end{equation}
.
<document source="<src>/index.md">
<bullet_list bullet="-">
<list_item>
<target ids="equation-mock-uuid">
<math_block classes="amsmath" docname="index" label="mock-uuid" nowrap="True" number="1" xml:space="preserve">
\begin{equation}
a = 1
\end{equation}
.

View File

@ -0,0 +1,31 @@
Basic note:
.
::: {note}
*hallo*
:::
.
<document source="<src>/index.md">
<note>
<paragraph>
<emphasis>
hallo
.
Admonition with options:
.
::: {admonition} A **title**
:class: other
*hallo*
:::
.
<document source="<src>/index.md">
<admonition classes="other">
<title>
A
<strong>
title
<paragraph>
<emphasis>
hallo
.

View File

@ -0,0 +1,56 @@
Simple:
.
Term **1**
: Definition *1*
second paragraph
Term 2
~ Definition 2a
~ Definition 2b
Term 3
: code block
: > quote
: other
.
<document source="<src>/index.md">
<definition_list classes="simple myst">
<definition_list_item>
<term>
Term
<strong>
1
<definition>
<paragraph>
Definition
<emphasis>
1
<paragraph>
second paragraph
<definition_list_item>
<term>
Term 2
<definition>
<paragraph>
Definition 2a
<definition>
<paragraph>
Definition 2b
<definition_list_item>
<term>
Term 3
<definition>
<literal_block language="none" xml:space="preserve">
code block
<definition>
<block_quote>
<paragraph>
quote
<definition>
<paragraph>
other
.

View File

@ -0,0 +1,159 @@
Test Directive 1:
.
```{restructuredtext-test-directive}
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=[], options={}, content: None
.
Test Directive 2:
.
```{restructuredtext-test-directive}
foo
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=[], options={}, content:
<literal_block xml:space="preserve">
foo
.
Test Directive 3:
.
```{restructuredtext-test-directive} foo
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=['foo'], options={}, content: None
.
Test Directive 4:
.
```{restructuredtext-test-directive} foo
bar
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=['foo'], options={}, content:
<literal_block xml:space="preserve">
bar
.
Test Directive 5:
.
```{restructuredtext-test-directive} foo bar
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=['foo bar'], options={}, content: None
.
Test Directive 6:
.
```{restructuredtext-test-directive} foo bar
baz
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=['foo bar'], options={}, content:
<literal_block xml:space="preserve">
baz
.
Test Directive 7:
.
```{restructuredtext-test-directive}
foo
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=[], options={}, content:
<literal_block xml:space="preserve">
foo
.
Test Directive Options 1:
.
```{restructuredtext-test-directive}
---
option1: a
option2: b
---
foo
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=[], options={'option1': 'a', 'option2': 'b'}, content:
<literal_block xml:space="preserve">
foo
.
Test Directive Options 2:
.
```{restructuredtext-test-directive}
:option1: a
:option2: b
foo
```
.
<document source="<src>/index.md">
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=[], options={'option1': 'a', 'option2': 'b'}, content:
<literal_block xml:space="preserve">
foo
.
Test Directive Options Error:
.
```{restructuredtext-test-directive}
:option1
:option2: b
foo
```
.
<document source="<src>/index.md">
<system_message level="3" line="1" source="<src>/index.md" type="ERROR">
<paragraph>
Directive 'restructuredtext-test-directive': Invalid options YAML: mapping values are not allowed here
in "<unicode string>", line 2, column 8:
option2: b
^
<literal_block xml:space="preserve">
:option1
:option2: b
foo
.
Unknown Directive:
.
```{unknown}
```
.
<document source="<src>/index.md">
<system_message level="3" line="1" source="<src>/index.md" type="ERROR">
<paragraph>
Unknown directive type "unknown".
<system_message level="1" line="1" source="<src>/index.md" type="INFO">
<paragraph>
No directive entry for "unknown" in module "docutils.parsers.rst.languages.en".
Trying "unknown" as canonical directive name.
.

View File

@ -0,0 +1,141 @@
note: content in first line only
.
```{note} a
```
.
arguments: []
body:
- a
content_offset: 0
options: {}
.
note: content in body only
.
```{note}
a
```
.
arguments: []
body:
- a
content_offset: 0
options: {}
.
note: content after option
.
```{note}
:class: name
a
```
.
arguments: []
body:
- a
content_offset: 1
options:
class:
- name
.
note: content after option with new line
.
```{note}
:class: name
a
```
.
arguments: []
body:
- a
content_offset: 2
options:
class:
- name
.
note: content after yaml option
.
```{note}
---
class: name
---
a
```
.
arguments: []
body:
- a
content_offset: 3
options:
class:
- name
.
note: content in first line and body
.
```{note} first line
:class: tip
body line
```
.
arguments: []
body:
- first line
- ''
- body line
content_offset: 1
options:
class:
- tip
.
admonition: no options, no new line
.
```{admonition} first line
body line
```
.
arguments:
- first line
body:
- body line
content_offset: 0
options: {}
.
admonition: no options, new line
.
```{admonition} first line
body line
```
.
arguments:
- first line
body:
- body line
content_offset: 1
options: {}
.
admonition: with options
.
```{admonition} first line
:class: tip
body line
```
.
arguments:
- first line
body:
- body line
content_offset: 2
options:
class:
- tip
.

View File

@ -0,0 +1,436 @@
--------------------------------
[attention] (`docutils.parsers.rst.directives.admonitions.Attention`):
.
```{attention}
a
```
.
<document source="notset">
<attention>
<paragraph>
a
.
--------------------------------
[caution] (`docutils.parsers.rst.directives.admonitions.Caution`):
.
```{caution}
a
```
.
<document source="notset">
<caution>
<paragraph>
a
.
--------------------------------
[danger] (`docutils.parsers.rst.directives.admonitions.Danger`):
.
```{danger}
a
```
.
<document source="notset">
<danger>
<paragraph>
a
.
--------------------------------
[error] (`docutils.parsers.rst.directives.admonitions.Error`):
.
```{error}
a
```
.
<document source="notset">
<error>
<paragraph>
a
.
--------------------------------
[important] (`docutils.parsers.rst.directives.admonitions.Important`):
.
```{important}
a
```
.
<document source="notset">
<important>
<paragraph>
a
.
--------------------------------
[note] (`docutils.parsers.rst.directives.admonitions.Note`):
.
```{note}
a
```
.
<document source="notset">
<note>
<paragraph>
a
.
--------------------------------
[tip] (`docutils.parsers.rst.directives.admonitions.Tip`):
.
```{tip}
a
```
.
<document source="notset">
<tip>
<paragraph>
a
.
--------------------------------
[hint] (`docutils.parsers.rst.directives.admonitions.Hint`):
.
```{hint}
a
```
.
<document source="notset">
<hint>
<paragraph>
a
.
--------------------------------
[warning] (`docutils.parsers.rst.directives.admonitions.Warning`):
.
```{warning}
a
```
.
<document source="notset">
<warning>
<paragraph>
a
.
--------------------------------
[admonition] (`docutils.parsers.rst.directives.admonitions.Admonition`):
.
```{admonition} myclass
a
```
.
<document source="notset">
<admonition classes="admonition-myclass">
<title>
myclass
<paragraph>
a
.
--------------------------------
[sidebar] (`docutils.parsers.rst.directives.body.Sidebar`):
.
```{sidebar} sidebar title
a
```
.
<document source="notset">
<sidebar>
<title>
sidebar title
<paragraph>
a
.
--------------------------------
[topic] (`docutils.parsers.rst.directives.body.Topic`):
.
```{topic} Topic Title
a
```
.
<document source="notset">
<topic>
<title>
Topic Title
<paragraph>
a
.
--------------------------------
[line-block] (`docutils.parsers.rst.directives.body.LineBlock`) SKIP: MockingError: MockState has not yet implemented attribute 'nest_line_block_lines'
.
```{line-block}
```
.
<document source="notset">
.
--------------------------------
[parsed-literal] (`docutils.parsers.rst.directives.body.ParsedLiteral`):
.
```{parsed-literal}
a
```
.
<document source="notset">
<literal_block xml:space="preserve">
a
.
--------------------------------
[rubric] (`docutils.parsers.rst.directives.body.Rubric`):
.
```{rubric} Rubric Title
```
.
<document source="notset">
<rubric>
Rubric Title
.
--------------------------------
[epigraph] (`docutils.parsers.rst.directives.body.Epigraph`):
.
```{epigraph}
a
-- attribution
```
.
<document source="notset">
<block_quote classes="epigraph">
<paragraph>
a
<attribution>
attribution
.
--------------------------------
[highlights] (`docutils.parsers.rst.directives.body.Highlights`):
.
```{highlights}
a
-- attribution
```
.
<document source="notset">
<block_quote classes="highlights">
<paragraph>
a
<attribution>
attribution
.
--------------------------------
[pull-quote] (`docutils.parsers.rst.directives.body.PullQuote`):
.
```{pull-quote}
a
-- attribution
```
.
<document source="notset">
<block_quote classes="pull-quote">
<paragraph>
a
<attribution>
attribution
.
--------------------------------
[compound] (`docutils.parsers.rst.directives.body.Compound`):
.
```{compound}
a
```
.
<document source="notset">
<compound>
<paragraph>
a
.
--------------------------------
[container] (`docutils.parsers.rst.directives.body.Container`):
.
```{container}
a
```
.
<document source="notset">
<container>
<paragraph>
a
.
--------------------------------
[image] (`docutils.parsers.rst.directives.images.Image`):
.
```{image} path/to/image
:alt: abc
:name: name
```
.
<document source="notset">
<image alt="abc" ids="name" names="name" uri="path/to/image">
.
--------------------------------
[raw] (`docutils.parsers.rst.directives.misc.Raw`):
.
```{raw} raw
a
```
.
<document source="notset">
<raw format="raw" xml:space="preserve">
a
.
--------------------------------
[class] (`docutils.parsers.rst.directives.misc.Class`):
.
```{class} myclass
a
```
.
<document source="notset">
<paragraph classes="myclass">
a
.
--------------------------------
[role] (`docutils.parsers.rst.directives.misc.Role`) + raw (`docutils.parsers.rst.roles.raw_role`):
.
```{role} raw-latex(raw)
:format: latex
```
{raw-latex}`\tag{content}`
.
<document source="notset">
<paragraph>
<raw classes="raw-latex" format="latex" xml:space="preserve">
\tag{content}
.
--------------------------------
[title] (`docutils.parsers.rst.directives.misc.Title`):
.
```{title} title
```
.
<document source="notset" title="title">
.
--------------------------------
[restructuredtext-test-directive] (`docutils.parsers.rst.directives.misc.TestDirective`):
.
```{restructuredtext-test-directive}
```
.
<document source="notset">
<system_message level="1" line="1" source="notset" type="INFO">
<paragraph>
Directive processed. Type="restructuredtext-test-directive", arguments=[], options={}, content: None
.
--------------------------------
[contents] (`docutils.parsers.rst.directives.parts.Contents`):
.
```{contents} Contents
```
.
<document source="notset">
<topic classes="contents" ids="contents" names="contents">
<title>
Contents
<pending>
.. internal attributes:
.transform: docutils.transforms.parts.Contents
.details:
.
--------------------------------
[sectnum] (`docutils.parsers.rst.directives.parts.Sectnum`):
.
```{sectnum}
```
.
<document source="notset">
<pending>
.. internal attributes:
.transform: docutils.transforms.parts.SectNum
.details:
.
--------------------------------
[header] (`docutils.parsers.rst.directives.parts.Header`):
.
```{header}
a
```
.
<document source="notset">
<decoration>
<header>
<paragraph>
a
.
--------------------------------
[footer] (`docutils.parsers.rst.directives.parts.Footer`):
.
```{footer}
a
```
.
<document source="notset">
<decoration>
<footer>
<paragraph>
a
.
--------------------------------
[target-notes] (`docutils.parsers.rst.directives.references.TargetNotes`):
.
```{target-notes}
```
.
<document source="notset">
<pending>
.. internal attributes:
.transform: docutils.transforms.references.TargetNotes
.details:
.

View File

@ -0,0 +1,131 @@
--------------------------------
[abbreviation] (`docutils.parsers.rst.roles.GenericRole`):
.
{abbreviation}`a`
.
<document source="notset">
<paragraph>
<abbreviation>
a
.
--------------------------------
[acronym] (`docutils.parsers.rst.roles.GenericRole`):
.
{acronym}`a`
.
<document source="notset">
<paragraph>
<acronym>
a
.
--------------------------------
[emphasis] (`docutils.parsers.rst.roles.GenericRole`):
.
{emphasis}`a`
.
<document source="notset">
<paragraph>
<emphasis>
a
.
--------------------------------
[literal] (`docutils.parsers.rst.roles.GenericRole`):
.
{literal}`a`
.
<document source="notset">
<paragraph>
<literal>
a
.
--------------------------------
[strong] (`docutils.parsers.rst.roles.GenericRole`):
.
{strong}`a`
.
<document source="notset">
<paragraph>
<strong>
a
.
--------------------------------
[subscript] (`docutils.parsers.rst.roles.GenericRole`):
.
{subscript}`a`
.
<document source="notset">
<paragraph>
<subscript>
a
.
--------------------------------
[superscript] (`docutils.parsers.rst.roles.GenericRole`):
.
{superscript}`a`
.
<document source="notset">
<paragraph>
<superscript>
a
.
--------------------------------
[title-reference] (`docutils.parsers.rst.roles.GenericRole`):
.
{title-reference}`t`
.
<document source="notset">
<paragraph>
<title_reference>
t
.
--------------------------------
[pep-reference] (`docutils.parsers.rst.roles.pep_reference_role`):
.
{pep-reference}`0`
.
<document source="notset">
<paragraph>
<reference refuri="https://peps.python.org/pep-0000">
PEP 0
.
--------------------------------
[rfc-reference] (`docutils.parsers.rst.roles.rfc_reference_role`):
.
{rfc-reference}`1`
.
<document source="notset">
<paragraph>
<reference refuri="https://tools.ietf.org/html/rfc1.html">
RFC 1
.
--------------------------------
[code] (`docutils.parsers.rst.roles.code_role`):
.
{code}`a`
.
<document source="notset">
<paragraph>
<literal classes="code">
a
.
--------------------------------
[math] (`docutils.parsers.rst.roles.math_role`):
.
{math}`a`
.
<document source="notset">
<paragraph>
<math>
a
.

View File

@ -0,0 +1,794 @@
Raw
.
foo
.
<document source="notset">
<paragraph>
foo
.
Hard-break
.
foo\
bar
.
<document source="notset">
<paragraph>
foo
<raw format="html" xml:space="preserve">
<br />
<raw format="latex" xml:space="preserve">
\\
bar
.
Strong:
.
**foo**
.
<document source="notset">
<paragraph>
<strong>
foo
.
Emphasis
.
*foo*
.
<document source="notset">
<paragraph>
<emphasis>
foo
.
Escaped Emphasis:
.
\*foo*
.
<document source="notset">
<paragraph>
*foo*
.
Mixed Inline
.
a *b* **c** `abc` \\*
.
<document source="notset">
<paragraph>
a
<emphasis>
b
<strong>
c
<literal>
abc
\*
.
Inline Code:
.
`foo`
.
<document source="notset">
<paragraph>
<literal>
foo
.
Heading:
.
# foo
.
<document source="notset">
<section ids="foo" names="foo">
<title>
foo
.
Heading Levels:
.
# a
## b
### c
# d
.
<document source="notset">
<section ids="a" names="a">
<title>
a
<section ids="b" names="b">
<title>
b
<section ids="c" names="c">
<title>
c
<section ids="d" names="d">
<title>
d
.
Block Code:
.
foo
.
<document source="notset">
<literal_block classes="code" xml:space="preserve">
foo
.
Fenced Code:
.
```sh
foo
```
.
<document source="notset">
<literal_block classes="code sh" xml:space="preserve">
foo
.
Fenced Code no language:
.
```
foo
```
.
<document source="notset">
<literal_block classes="code" xml:space="preserve">
foo
.
Fenced Code no language with trailing whitespace:
.
```
foo
```
.
<document source="notset">
<literal_block classes="code" xml:space="preserve">
foo
.
Image empty:
.
![]()
.
<document source="notset">
<paragraph>
<image alt="" uri="">
.
Image with alt and title:
.
![alt](src "title")
.
<document source="notset">
<paragraph>
<image alt="alt" title="title" uri="src">
.
Image with escapable html:
.
![alt](http://www.google<>.com)
.
<document source="notset">
<paragraph>
<image alt="alt" uri="http://www.google%3C%3E.com">
.
Block Quote:
.
> *foo*
.
<document source="notset">
<block_quote>
<paragraph>
<emphasis>
foo
.
Bullet List:
.
- *foo*
* bar
.
<document source="notset">
<bullet_list bullet="-">
<list_item>
<paragraph>
<emphasis>
foo
<bullet_list bullet="*">
<list_item>
<paragraph>
bar
.
Nested Bullets
.
- a
- b
- c
- d
.
<document source="notset">
<bullet_list bullet="-">
<list_item>
<paragraph>
a
<bullet_list bullet="-">
<list_item>
<paragraph>
b
<bullet_list bullet="-">
<list_item>
<paragraph>
c
<list_item>
<paragraph>
d
.
Enumerated List:
.
1. *foo*
1) bar
para
10. starting
11. enumerator
.
<document source="notset">
<enumerated_list enumtype="arabic" prefix="" suffix=".">
<list_item>
<paragraph>
<emphasis>
foo
<enumerated_list enumtype="arabic" prefix="" suffix=")">
<list_item>
<paragraph>
bar
<paragraph>
para
<enumerated_list enumtype="arabic" prefix="" start="10" suffix=".">
<list_item>
<paragraph>
starting
<list_item>
<paragraph>
enumerator
.
Nested Enumrated List:
.
1. a
2. b
1. c
.
<document source="notset">
<enumerated_list enumtype="arabic" prefix="" suffix=".">
<list_item>
<paragraph>
a
<list_item>
<paragraph>
b
<enumerated_list enumtype="arabic" prefix="" suffix=".">
<list_item>
<paragraph>
c
.
Sphinx Role containing backtick:
.
{code}``a=1{`}``
.
<document source="notset">
<paragraph>
<literal classes="code">
a=1{`}
.
Target:
.
(target)=
.
<document source="notset">
<target ids="target" names="target">
.
Target with whitespace:
.
(target with space)=
.
<document source="notset">
<target ids="target-with-space" names="target\ with\ space">
.
Referencing:
.
(target)=
Title
=====
[alt1](target)
[](target2)
[alt2](https://www.google.com)
[alt3](#target3)
.
<document source="notset">
<target ids="target" names="target">
<section ids="title" names="title">
<title>
Title
<paragraph>
<reference refname="target">
alt1
<paragraph>
<reference refname="target2">
<paragraph>
<reference refuri="https://www.google.com">
alt2
<paragraph>
<reference refname="#target3">
alt3
.
Comments:
.
line 1
% a comment
line 2
.
<document source="notset">
<paragraph>
line 1
<comment xml:space="preserve">
a comment
<paragraph>
line 2
.
Block Break:
.
+++ string
.
<document source="notset">
<comment classes="block_break" xml:space="preserve">
string
.
Link Reference:
.
[name][key]
[key]: https://www.google.com "a title"
.
<document source="notset">
<paragraph>
<reference refuri="https://www.google.com" title="a title">
name
.
Link Reference short version:
.
[name]
[name]: https://www.google.com "a title"
.
<document source="notset">
<paragraph>
<reference refuri="https://www.google.com" title="a title">
name
.
Block Quotes:
.
```{epigraph}
a b*c*
-- a**b**
```
.
<document source="notset">
<block_quote classes="epigraph">
<paragraph>
a b
<emphasis>
c
<attribution>
a
<strong>
b
.
Link Definition in directive:
.
```{note}
[a]
```
[a]: link
.
<document source="notset">
<note>
<paragraph>
<reference refname="link">
a
.
Link Definition in nested directives:
.
```{note}
[ref1]: link
```
```{note}
[ref1]
[ref2]
```
```{note}
[ref2]: link
```
.
<document source="notset">
<note>
<note>
<paragraph>
<reference refname="link">
ref1
[ref2]
<note>
.
Footnotes:
.
[^a]
[^a]: footnote*text*
.
<document source="notset">
<paragraph>
<footnote_reference auto="1" ids="id1" refname="a">
<transition classes="footnotes">
<footnote auto="1" ids="a" names="a">
<paragraph>
footnote
<emphasis>
text
.
Footnotes nested blocks:
.
[^a]
[^a]: footnote*text*
abc
xyz
> a
- b
c
finish
.
<document source="notset">
<paragraph>
<footnote_reference auto="1" ids="id1" refname="a">
<paragraph>
finish
<transition classes="footnotes">
<footnote auto="1" ids="a" names="a">
<paragraph>
footnote
<emphasis>
text
<paragraph>
abc
xyz
<block_quote>
<paragraph>
a
<bullet_list bullet="-">
<list_item>
<paragraph>
b
<paragraph>
c
.
Front Matter:
.
---
a: 1
b: foo
c:
d: 2
---
.
<document source="notset">
<field_list>
<field>
<field_name>
a
<field_body>
<paragraph>
<literal>
1
<field>
<field_name>
b
<field_body>
<paragraph>
<literal>
foo
<field>
<field_name>
c
<field_body>
<paragraph>
<literal>
{"d": 2}
.
Front Matter Biblio:
.
---
author: Chris Sewell
authors: Chris Sewell, Chris Hodgraf
organization: EPFL
address: |
1 Cedar Park Close
Thundersley
Essex
contact: <https://example.com>
version: 1.0
revision: 1.1
status: good
date: 2/12/1985
copyright: MIT
dedication: |
To my *homies*
abstract:
Something something **dark** side
other: Something else
---
.
<document source="notset">
<field_list>
<field>
<field_name>
author
<field_body>
<paragraph>
Chris Sewell
<field>
<field_name>
authors
<field_body>
<paragraph>
Chris Sewell, Chris Hodgraf
<field>
<field_name>
organization
<field_body>
<paragraph>
EPFL
<field>
<field_name>
address
<field_body>
<paragraph>
1 Cedar Park Close
Thundersley
Essex
<field>
<field_name>
contact
<field_body>
<paragraph>
<reference refuri="https://example.com">
https://example.com
<field>
<field_name>
version
<field_body>
<paragraph>
1.0
<field>
<field_name>
revision
<field_body>
<paragraph>
1.1
<field>
<field_name>
status
<field_body>
<paragraph>
good
<field>
<field_name>
date
<field_body>
<paragraph>
2/12/1985
<field>
<field_name>
copyright
<field_body>
<paragraph>
MIT
<field>
<field_name>
dedication
<field_body>
<paragraph>
To my
<emphasis>
homies
<field>
<field_name>
abstract
<field_body>
<paragraph>
Something something
<strong>
dark
side
<field>
<field_name>
other
<field_body>
<paragraph>
<literal>
Something else
.
Front Matter Bad Yaml:
.
---
a: {
---
.
<document source="notset">
<system_message level="2" line="1" source="notset" type="WARNING">
<paragraph>
Malformed YAML [myst.topmatter]
.
Front Matter HTML Meta
.
---
myst:
html_meta:
keywords: Sphinx, documentation, builder
description lang=en: An amusing story
description lang=fr: Un histoire amusant
http-equiv=Content-Type: text/html; charset=ISO-8859-1
---
.
<document source="notset">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="Sphinx, documentation, builder" name="keywords">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="An amusing story" lang="en" name="description">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="Un histoire amusant" lang="fr" name="description">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="text/html; charset=ISO-8859-1" http-equiv="Content-Type">
.
Full Test:
.
---
a: 1
---
(target)=
# header 1
## sub header 1
a *b* **c** `abc`
## sub header 2
x y [a](http://www.xyz.com) z
---
# header 2
```::python {a=1}
a = 1
```
[](target)
.
<document source="notset">
<field_list>
<field>
<field_name>
a
<field_body>
<paragraph>
<literal>
1
<target ids="target" names="target">
<section ids="header-1" names="header\ 1">
<title>
header 1
<section ids="sub-header-1" names="sub\ header\ 1">
<title>
sub header 1
<paragraph>
a
<emphasis>
b
<strong>
c
<literal>
abc
<section ids="sub-header-2" names="sub\ header\ 2">
<title>
sub header 2
<paragraph>
x y
<reference refuri="http://www.xyz.com">
a
z
<transition>
<section ids="header-2" names="header\ 2">
<title>
header 2
<literal_block classes="code ::python" xml:space="preserve">
a = 1
<paragraph>
<reference refname="target">
.

View File

@ -0,0 +1,139 @@
[dollarmath] --myst-enable-extensions=dollarmath
.
$foo$
a $foo
bar$ b
$$foo$$
$$
a = 1
$$
.
<document source="<string>">
<paragraph>
<math>
foo
<paragraph>
a
<math>
foo
bar
b
<math_block nowrap="False" number="True" xml:space="preserve">
foo
<math_block nowrap="False" number="True" xml:space="preserve">
a = 1
.
[amsmath] --myst-enable-extensions=amsmath
.
\begin{equation} a \end{equation}
\begin{equation}
a
\end{equation}
\begin{equation*}
a
\end{equation*}
.
<document source="<string>">
<math_block classes="amsmath" nowrap="True" numbered="True" xml:space="preserve">
\begin{equation} a \end{equation}
<math_block classes="amsmath" nowrap="True" numbered="True" xml:space="preserve">
\begin{equation}
a
\end{equation}
<math_block classes="amsmath" nowrap="True" xml:space="preserve">
\begin{equation*}
a
\end{equation*}
.
[deflist] --myst-enable-extensions=deflist
.
term
: definition
.
<document source="<string>">
<definition_list classes="simple myst">
<definition_list_item>
<term>
term
<definition>
<paragraph>
definition
.
[fieldlist] --myst-enable-extensions=fieldlist
.
:name: value
.
<document source="<string>">
<docinfo>
<field classes="name">
<field_name>
name
<field_body>
<paragraph>
value
.
[colon_fence] --myst-enable-extensions=colon_fence
.
:::{note}
content
:::
.
<document source="<string>">
<note>
<paragraph>
content
.
[replacements] --myst-enable-extensions=replacements
.
(c) (C) (r) (R) (tm) (TM) (p) (P) +- ...
.
<document source="<string>">
<paragraph>
© © ® ® ™ ™ § § ± …
.
[strikethrough] --myst-enable-extensions=strikethrough
.
~~foo~~
.
<document source="<string>">
<paragraph>
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Strikethrough is currently only supported in HTML output [myst.strikethrough]
<raw format="html" xml:space="preserve">
<s>
foo
<raw format="html" xml:space="preserve">
</s>
.
[tasklist] --myst-enable-extensions=tasklist
.
- [ ] foo
- [x] bar
.
<document source="<string>">
<bullet_list bullet="-" classes="contains-task-list">
<list_item classes="task-list-item">
<paragraph>
<raw format="html" xml:space="preserve">
<input class="task-list-item-checkbox" disabled="disabled" type="checkbox">
foo
<list_item classes="task-list-item">
<paragraph>
<raw format="html" xml:space="preserve">
<input class="task-list-item-checkbox" checked="checked" disabled="disabled" type="checkbox">
bar
.

View File

@ -0,0 +1,75 @@
Inline Math:
.
$foo$
.
<document source="<src>/index.md">
<paragraph>
<math>
foo
.
Inline Math, multi-line:
.
a $foo
bar$ b
.
<document source="<src>/index.md">
<paragraph>
a
<math>
foo
bar
b
.
Inline Math, multi-line with line break (invalid):
.
a $foo
bar$ b
.
<document source="<src>/index.md">
<paragraph>
a $foo
<paragraph>
bar$ b
.
Math Block:
.
$$foo$$
.
<document source="<src>/index.md">
<math_block nowrap="False" number="True" xml:space="preserve">
foo
.
Math Block With Equation Label:
.
$$foo$$ (abc)
.
<document source="<src>/index.md">
<target ids="equation-abc">
<math_block docname="index" label="abc" nowrap="False" number="1" xml:space="preserve">
foo
.
Math Block multiple:
.
$$
a = 1
$$
$$
b = 2
$$ (a)
.
<document source="<src>/index.md">
<math_block nowrap="False" number="True" xml:space="preserve">
a = 1
<target ids="equation-a">
<math_block docname="index" label="a" nowrap="False" number="1" xml:space="preserve">
b = 2
.

View File

@ -0,0 +1,23 @@
eval-rst link
.
```{eval-rst}
`MyST Parser <https://myst-parser.readthedocs.io/>`_
```
.
<document source="<src>/index.md">
<paragraph>
<reference name="MyST Parser" refuri="https://myst-parser.readthedocs.io/">
MyST Parser
<target ids="myst-parser" names="myst\ parser" refuri="https://myst-parser.readthedocs.io/">
.
eval-rst bold
.
```{eval-rst}
**bold**
.
<document source="<src>/index.md">
<paragraph>
<strong>
bold
.

View File

@ -0,0 +1,62 @@
Basic Include:
.
```{include} other.md
```
.
<document source="tmpdir/test.md">
<paragraph>
a
b
c
.
Include with Front Matter (should be ignored):
.
```{include} fmatter.md
```
.
<document source="tmpdir/test.md">
<paragraph>
b
.
Include Literal:
.
```{include} other.md
:literal: True
```
.
<document source="tmpdir/test.md">
<literal_block source="tmpdir/other.md" xml:space="preserve">
a
b
c
.
Include Literal, line range:
.
```{include} other.md
:literal: True
:start-line: 1
:end-line: 2
```
.
<document source="tmpdir/test.md">
<literal_block source="tmpdir/other.md" xml:space="preserve">
b
.
Include code:
.
```{include} other.md
:code: md
```
.
<document source="tmpdir/test.md">
<literal_block classes="code md" source="tmpdir/other.md" xml:space="preserve">
a
b
c
.

View File

@ -0,0 +1,24 @@
Missing path:
.
```{include}
```
.
tmpdir/test.md:1: (ERROR/3) Directive 'include': 1 argument(s) required, 0 supplied
.
Non-existent path:
.
```{include} other.md
```
.
tmpdir/test.md:1: (SEVERE/4) Directive "include": error reading file: tmpdir/other.md
[Errno 2] No such file or directory: 'tmpdir/other.md'.
.
Error in include file:
.
```{include} bad.md
```
.
tmpdir/bad.md:2: (ERROR/3) Unknown interpreted text role "a".
.

View File

@ -0,0 +1,176 @@
[title-to-header] --myst-title-to-header="yes"
.
---
title: "The title *nested syntax*"
---
# Other header
.
<document source="<string>">
<docinfo>
<field classes="title">
<field_name>
title
<field_body>
<paragraph>
<literal>
The title *nested syntax*
<section ids="the-title-nested-syntax" names="the\ title\ nested\ syntax">
<title>
The title
<emphasis>
nested syntax
<section ids="other-header" names="other\ header">
<title>
Other header
.
[linkify] --myst-enable-extensions=linkify
.
www.example.com
.
<document source="<string>">
<paragraph>
<reference refuri="http://www.example.com">
www.example.com
.
[gfm-strikethrough] --myst-gfm-only="yes"
.
~~strike~~
.
<document source="<string>">
<paragraph>
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Strikethrough is currently only supported in HTML output [myst.strikethrough]
<raw format="html" xml:space="preserve">
<s>
strike
<raw format="html" xml:space="preserve">
</s>
<string>:1: (WARNING/2) Strikethrough is currently only supported in HTML output [myst.strikethrough]
.
[gfm-disallowed-html] --myst-gfm-only="yes"
.
<strong> <title> <style> <em>
<blockquote>
<xmp> is disallowed. <XMP> is also disallowed.
</blockquote>
.
<document source="<string>">
<paragraph>
<raw format="html" xml:space="preserve">
<strong>
<raw format="html" xml:space="preserve">
&lt;title>
<raw format="html" xml:space="preserve">
&lt;style>
<raw format="html" xml:space="preserve">
<em>
<raw format="html" xml:space="preserve">
<blockquote>
&lt;xmp> is disallowed. &lt;XMP> is also disallowed.
</blockquote>
.
[gfm-autolink] --myst-gfm-only="yes"
.
www.commonmark.org
Visit www.commonmark.org/help for more information.
www.google.com/search?q=Markup+(business)
www.google.com/search?q=Markup+(business)))
(www.google.com/search?q=Markup+(business))
(www.google.com/search?q=Markup+(business)
www.google.com/search?q=(business))+ok
www.google.com/search?q=commonmark&hl=en
www.google.com/search?q=commonmark&hl;
www.commonmark.org/he<lp
.
<document source="<string>">
<paragraph>
<reference refuri="http://www.commonmark.org">
www.commonmark.org
<paragraph>
Visit
<reference refuri="http://www.commonmark.org/help">
www.commonmark.org/help
for more information.
<paragraph>
<reference refuri="http://www.google.com/search?q=Markup+(business)">
www.google.com/search?q=Markup+(business)
<paragraph>
<reference refuri="http://www.google.com/search?q=Markup+(business)">
www.google.com/search?q=Markup+(business)
))
<paragraph>
(
<reference refuri="http://www.google.com/search?q=Markup+(business)">
www.google.com/search?q=Markup+(business)
)
<paragraph>
(
<reference refuri="http://www.google.com/search?q=Markup+(business)">
www.google.com/search?q=Markup+(business)
<paragraph>
<reference refuri="http://www.google.com/search?q=(business)">
www.google.com/search?q=(business)
)+ok
<paragraph>
<reference refuri="http://www.google.com/search?q=commonmark&amp;hl=en">
www.google.com/search?q=commonmark&hl=en
<paragraph>
<reference refuri="http://www.google.com/search?q=commonmark&amp;hl">
www.google.com/search?q=commonmark&hl
;
<paragraph>
<reference refuri="http://www.commonmark.org/he">
www.commonmark.org/he
<lp
.
[attrs_image] --myst-enable-extensions=attrs_image
.
![a](b){#id .a width="100%" align=center height=20px}{.b}
.
<document source="<string>">
<paragraph>
<image align="center" alt="a" classes="a b" height="20px" ids="id" names="id" uri="b" width="100%">
.
[attrs_image_warnings] --myst-enable-extensions=attrs_image
.
![a](b){width=1x height=2x align=other }
.
<document source="<string>">
<paragraph>
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid width value for image: '1x' [myst.image]
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid height value for image: '2x' [myst.image]
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid align value for image: 'other' [myst.image]
<image alt="a" uri="b">
<string>:1: (WARNING/2) Invalid width value for image: '1x' [myst.image]
<string>:1: (WARNING/2) Invalid height value for image: '2x' [myst.image]
<string>:1: (WARNING/2) Invalid align value for image: 'other' [myst.image]
.

View File

@ -0,0 +1,180 @@
Duplicate Reference definitions:
.
[a]: b
[a]: c
.
<string>:2: (WARNING/2) Duplicate reference definition: A [myst.ref]
.
Missing Reference:
.
[a](b)
.
<string>:1: (ERROR/3) Unknown target name: "b".
.
Unknown role:
.
abc
{xyz}`a`
.
<string>:3: (ERROR/3) Unknown interpreted text role "xyz".
.
Unknown directive:
.
```{xyz}
```
.
<string>:2: (ERROR/3) Unknown directive type "xyz".
.
Bad Front Matter:
.
---
a: {
---
.
<string>:1: (WARNING/2) Malformed YAML [myst.topmatter]
.
Unknown Front Matter myst key:
.
---
myst:
unknown: true
---
.
<string>:1: (WARNING/2) Unknown field: unknown [myst.topmatter]
.
Invalid Front Matter myst key:
.
---
myst:
title_to_header: 1
url_schemes: [1]
substitutions:
key: []
---
.
<string>:1: (WARNING/2) 'title_to_header' must be of type <class 'bool'> (got 1 that is a <class 'int'>). [myst.topmatter]
<string>:1: (WARNING/2) 'url_schemes[0]' must be of type <class 'str'> (got 1 that is a <class 'int'>). [myst.topmatter]
<string>:1: (WARNING/2) 'substitutions['key']' must be of type (<class 'str'>, <class 'int'>, <class 'float'>) (got [] that is a <class 'list'>). [myst.topmatter]
.
Bad HTML Meta
.
---
myst:
html_meta:
name noequals: value
---
.
<string>:: (ERROR/3) Error parsing meta tag attribute "name noequals": no '=' in noequals.
.
Directive parsing error:
.
```{class}
```
.
<string>:2: (ERROR/3) Directive 'class': 1 argument(s) required, 0 supplied
.
Directive run error:
.
```{date}
x
```
.
<string>:2: (ERROR/3) Invalid context: the "date" directive can only be used within a substitution definition.
.
Do not start headings at H1:
.
## title 1
.
<string>:1: (WARNING/2) Document headings start at H2, not H1 [myst.header]
.
Non-consecutive headings:
.
# title 1
### title 3
.
<string>:2: (WARNING/2) Non-consecutive header level increase; H1 to H3 [myst.header]
.
multiple footnote definitions
.
[^a]
[^a]: definition 1
[^a]: definition 2
.
<string>:: (WARNING/2) Multiple footnote definitions found for label: 'a' [myst.footnote]
.
Warnings in eval-rst
.
some external
lines
```{eval-rst}
some internal
lines
.. unknown:: some text
:unknown:`a`
```
.
<string>:10: (ERROR/3) Unknown directive type "unknown".
.. unknown:: some text
<string>:12: (ERROR/3) Unknown interpreted text role "unknown".
.
bad-option-value
.
```{note}
:class: [1]
```
.
<string>:1: (ERROR/3) Directive 'note': option "class" value not string (enclose with ""): [1]
:class: [1]
.
header nested in admonition
.
```{note}
# Header
```
.
<string>:2: (WARNING/2) Disallowed nested header found, converting to rubric [myst.nested_header]
.
nested parse warning
.
Paragraph
```{note}
:class: abc
:name: xyz
{unknown}`a`
```
.
<string>:7: (ERROR/3) Unknown interpreted text role "unknown".
.

View File

@ -0,0 +1,441 @@
default-role (`sphinx.directives.DefaultRole`):
.
```{default-role}
```
.
<document source="<src>/index.md">
.
default-domain (`sphinx.directives.DefaultDomain`):
.
```{default-domain} mydomain
```
.
<document source="<src>/index.md">
.
object (`sphinx.directives.ObjectDescription`):
.
```{object} something
```
.
<document source="<src>/index.md">
<index entries="">
<desc classes="object" desctype="object" domain="" noindex="False" objtype="object">
<desc_signature classes="sig sig-object">
<desc_name classes="sig-name descname" xml:space="preserve">
something
<desc_content>
.
highlight (`sphinx.directives.code.Highlight`):
.
```{highlight} something
```
.
<document source="<src>/index.md">
<highlightlang force="False" lang="something" linenothreshold="9223372036854775807">
.
code-block (`sphinx.directives.code.CodeBlock`):
.
```{code-block}
:dedent:
a=1
```
.
<document source="<src>/index.md">
<literal_block force="False" highlight_args="{}" language="default" xml:space="preserve">
a=1
.
sourcecode (`sphinx.directives.code.CodeBlock`):
.
```{sourcecode}
```
.
<document source="<src>/index.md">
<literal_block force="False" highlight_args="{}" language="default" xml:space="preserve">
.
SKIP: Tested in sphinx builds
.
```{literalinclude} /path/to/file
```
.
<document source="notset">
<system_message level="2" line="1" source="notset" type="WARNING">
<paragraph>
Include file '/srcdir/path/to/file' not found or reading it failed
.
toctree (`sphinx.directives.other.TocTree`):
.
```{toctree}
```
.
<document source="<src>/index.md">
<compound classes="toctree-wrapper">
<toctree caption="True" entries="" glob="False" hidden="False" includefiles="" includehidden="False" maxdepth="-1" numbered="0" parent="index" titlesonly="False">
.
sectionauthor (`sphinx.directives.other.Author`):
.
```{sectionauthor} bob geldof
```
.
<document source="<src>/index.md">
.
moduleauthor (`sphinx.directives.other.Author`):
.
```{moduleauthor} ringo starr
```
.
<document source="<src>/index.md">
.
codeauthor (`sphinx.directives.other.Author`):
.
```{codeauthor} paul mcartney
```
.
<document source="<src>/index.md">
.
index (`sphinx.directives.other.Index`):
.
```{index} something
```
.
<document source="<src>/index.md">
<index entries="('single',\ 'something',\ 'index-0',\ '',\ None)" inline="False">
<target ids="index-0">
.
seealso (`sphinx.directives.other.SeeAlso`):
.
```{seealso}
a
```
.
<document source="<src>/index.md">
<seealso>
<paragraph>
a
.
tabularcolumns (`sphinx.directives.other.TabularColumns`):
.
```{tabularcolumns} spec
```
.
<document source="<src>/index.md">
<tabular_col_spec spec="spec">
.
centered (`sphinx.directives.other.Centered`):
.
```{centered} text
```
.
<document source="<src>/index.md">
<centered>
text
.
acks (`sphinx.directives.other.Acks`):
.
```{acks}
- name
```
.
<document source="<src>/index.md">
<acks>
<bullet_list bullet="-">
<list_item>
<paragraph>
name
.
hlist (`sphinx.directives.other.HList`):
.
```{hlist}
- item
```
.
<document source="<src>/index.md">
<hlist ncolumns="2">
<hlistcol>
<bullet_list>
<list_item>
<paragraph>
item
<hlistcol>
<bullet_list>
.
only (`sphinx.directives.other.Only`):
.
```{only} expr
```
.
<document source="<src>/index.md">
<only expr="expr">
.
SKIP: Tested in sphinx builds
.
```{include} path/to/include
```
.
<document source="notset">
.
figure (`sphinx.directives.patches.Figure`):
.
```{figure} path/to/figure
*caption*
legend
```
.
<document source="<src>/index.md">
<figure>
<image uri="path/to/figure">
<caption>
<emphasis>
caption
<legend>
<paragraph>
legend
.
SKIP: MockingError: MockState has not yet implemented attribute 'nested_list_parse'
.
```{meta}
foo
```
.
<document source="notset">
.
table (`sphinx.directives.patches.RSTTable`):
.
```{table} *title*
:name: name
| a | b |
|---|---|
| 1 | 2 |
```
.
<document source="<src>/index.md">
<table classes="colwidths-auto" ids="name" names="name">
<title>
<emphasis>
title
<tgroup cols="2">
<colspec colwidth="50">
<colspec colwidth="50">
<thead>
<row>
<entry>
<paragraph>
a
<entry>
<paragraph>
b
<tbody>
<row>
<entry>
<paragraph>
1
<entry>
<paragraph>
2
.
csv-table (`sphinx.directives.patches.CSVTable`):
.
```{csv-table}
"Albatross", 2.99, "On a stick!"
```
.
<document source="<src>/index.md">
<table>
<tgroup cols="3">
<colspec colwidth="33">
<colspec colwidth="33">
<colspec colwidth="33">
<tbody>
<row>
<entry>
<paragraph>
Albatross
<entry>
<paragraph>
2.99
<entry>
<paragraph>
On a stick!
.
list-table (`sphinx.directives.patches.ListTable`):
.
```{list-table}
* - item
```
.
<document source="<src>/index.md">
<table>
<tgroup cols="1">
<colspec colwidth="100">
<tbody>
<row>
<entry>
<paragraph>
item
.
code (`sphinx.directives.patches.Code`):
.
```{code} python
a
```
.
<document source="<src>/index.md">
<literal_block force="False" highlight_args="{}" language="python" xml:space="preserve">
a
.
math (`sphinx.directives.patches.MathDirective`):
.
```{math}
```
.
<document source="<src>/index.md">
<math_block docname="index" label="True" nowrap="False" number="True" xml:space="preserve">
.
deprecated (`sphinx.domains.changeset.VersionChange`):
.
```{deprecated} 0.3
```
.
<document source="<src>/index.md">
<versionmodified type="deprecated" version="0.3">
<paragraph translatable="False">
<inline classes="versionmodified deprecated">
Deprecated since version 0.3.
.
versionadded (`sphinx.domains.changeset.VersionChange`):
.
```{versionadded} 0.2
```
.
<document source="<src>/index.md">
<versionmodified type="versionadded" version="0.2">
<paragraph translatable="False">
<inline classes="versionmodified added">
New in version 0.2.
.
versionchanged (`sphinx.domains.changeset.VersionChange`):
.
```{versionchanged} 0.1
```
.
<document source="<src>/index.md">
<versionmodified type="versionchanged" version="0.1">
<paragraph translatable="False">
<inline classes="versionmodified changed">
Changed in version 0.1.
.
glossary (`sphinx.domains.std.Glossary`):
.
```{glossary}
term 1 : A
term 2 : B
Definition of both terms.
```
.
<document source="<src>/index.md">
<glossary>
<definition_list classes="glossary">
<definition_list_item>
<term ids="term-term-1">
term 1
<index entries="('single',\ 'term\ 1',\ 'term-term-1',\ 'main',\ 'A')">
<term ids="term-term-2">
term 2
<index entries="('single',\ 'term\ 2',\ 'term-term-2',\ 'main',\ 'B')">
<definition>
<paragraph>
Definition of both terms.
.
SPHINX4-SKIP productionlist (`sphinx.domains.std.ProductionList`):
.
```{productionlist} try_stmt: try1_stmt | try2_stmt
```
.
<document source="<src>/index.md">
<productionlist>
<production ids="grammar-token-try_stmt" tokenname="try_stmt" xml:space="preserve">
try1_stmt | try2_stmt
.
cmdoption (`sphinx.domains.std.Cmdoption`):
.
```{cmdoption} a
```
.
<document source="<src>/index.md">
<index entries="('pair',\ 'command\ line\ option;\ a',\ 'cmdoption-arg-a',\ '',\ None)">
<desc classes="std cmdoption" desctype="cmdoption" domain="std" noindex="False" objtype="cmdoption">
<desc_signature allnames="a" classes="sig sig-object" ids="cmdoption-arg-a">
<desc_name classes="sig-name descname" xml:space="preserve">
a
<desc_addname classes="sig-prename descclassname" xml:space="preserve">
<desc_content>
.
rst:directive (`sphinx.domains.rst.ReSTDirective`):
.
```{rst:directive} a
```
.
<document source="<src>/index.md">
<index entries="('single',\ 'a\ (directive)',\ 'directive-a',\ '',\ None)">
<desc classes="rst directive" desctype="directive" domain="rst" noindex="False" objtype="directive">
<desc_signature classes="sig sig-object" ids="directive-a">
<desc_name classes="sig-name descname" xml:space="preserve">
.. a::
<desc_content>
.
SPHINX4-SKIP rst:directive:option (`sphinx.domains.rst.ReSTDirectiveOption`):
.
```{rst:directive:option} a
```
.
<document source="<src>/index.md">
<index entries="('single',\ ':a:\ (directive\ option)',\ 'directive-option-a',\ '',\ 'A')">
<desc classes="rst directive:option" desctype="directive:option" domain="rst" noindex="False" objtype="directive:option">
<desc_signature classes="sig sig-object" ids="directive-option-a">
<desc_name classes="sig-name descname" xml:space="preserve">
:a:
<desc_content>
.

View File

@ -0,0 +1,632 @@
c:func (`sphinx.domains.c.CXRefRole`):
.
{c:func}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="c" refexplicit="False" reftarget="a" reftype="func" refwarn="False">
<literal classes="xref c c-func">
a()
.
c:member (`sphinx.domains.c.CObject`):
.
{c:member}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="c" refexplicit="False" reftarget="a" reftype="member" refwarn="False">
<literal classes="xref c c-member">
a
.
c:macro (`sphinx.domains.c.CObject`):
.
{c:macro}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="c" refexplicit="False" reftarget="a" reftype="macro" refwarn="False">
<literal classes="xref c c-macro">
a
.
c:data (`sphinx.domains.c.CXRefRole`):
.
{c:data}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="c" refexplicit="False" reftarget="a" reftype="data" refwarn="False">
<literal classes="xref c c-data">
a
.
c:type (`sphinx.domains.c.CObject`):
.
{c:type}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="c" refexplicit="False" reftarget="a" reftype="type" refwarn="False">
<literal classes="xref c c-type">
a
.
cpp:any (`sphinx.domains.cpp.CPPXRefRole`):
.
{cpp:any}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="any" refwarn="False">
<literal classes="xref cpp cpp-any">
a
.
cpp:class (`sphinx.domains.cpp.CPPClassObject`):
.
{cpp:class}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="class" refwarn="False">
<literal classes="xref cpp cpp-class">
a
.
cpp:struct (`sphinx.domains.cpp.CPPClassObject`):
.
{cpp:struct}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="struct" refwarn="False">
<literal classes="xref cpp cpp-struct">
a
.
cpp:union (`sphinx.domains.cpp.CPPUnionObject`):
.
{cpp:union}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="union" refwarn="False">
<literal classes="xref cpp cpp-union">
a
.
cpp:func (`sphinx.domains.cpp.CPPXRefRole`):
.
{cpp:func}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="func" refwarn="False">
<literal classes="xref cpp cpp-func">
a()
.
cpp:member (`sphinx.domains.cpp.CPPMemberObject`):
.
{cpp:member}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="member" refwarn="False">
<literal classes="xref cpp cpp-member">
a
.
cpp:var (`sphinx.domains.cpp.CPPMemberObject`):
.
{cpp:var}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="var" refwarn="False">
<literal classes="xref cpp cpp-var">
a
.
cpp:type (`sphinx.domains.cpp.CPPTypeObject`):
.
{cpp:type}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="type" refwarn="False">
<literal classes="xref cpp cpp-type">
a
.
cpp:concept (`sphinx.domains.cpp.CPPConceptObject`):
.
{cpp:concept}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="concept" refwarn="False">
<literal classes="xref cpp cpp-concept">
a
.
cpp:enum (`sphinx.domains.cpp.CPPEnumObject`):
.
{cpp:enum}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="enum" refwarn="False">
<literal classes="xref cpp cpp-enum">
a
.
cpp:enumerator (`sphinx.domains.cpp.CPPEnumeratorObject`):
.
{cpp:enumerator}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="cpp" refexplicit="False" reftarget="a" reftype="enumerator" refwarn="False">
<literal classes="xref cpp cpp-enumerator">
a
.
SKIP cpp:expr (`sphinx.domains.cpp.CPPExprRole`):
.
{cpp:expr}`a`
.
<document source="notset">
<paragraph>
<desc_inline classes="cpp-expr sig sig-inline cpp">
<pending_xref classname="True" cpp:parent_key="<sphinx.domains.cpp.LookupKey object at 0x7f948a6a73d0>" modname="True" refdomain="cpp" reftarget="a" reftype="identifier">
<desc_sig_name classes="n">
a
.
SKIP cpp:texpr (`sphinx.domains.cpp.CPPExprRole`):
.
{cpp:texpr}`a`
.
<document source="notset">
<paragraph>
<desc_inline classes="cpp-texpr sig sig-inline cpp">
<pending_xref classname="True" cpp:parent_key="<sphinx.domains.cpp.LookupKey object at 0x7fac40b5f950>" modname="True" refdomain="cpp" reftarget="a" reftype="identifier">
<desc_sig_name classes="n">
a
.
js:func (`sphinx.domains.javascript.JSXRefRole`):
.
{js:func}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref js:module="True" js:object="True" refdoc="index" refdomain="js" refexplicit="False" reftarget="a" reftype="func" refwarn="False">
<literal classes="xref js js-func">
a()
.
js:meth (`sphinx.domains.javascript.JSXRefRole`):
.
{js:meth}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref js:module="True" js:object="True" refdoc="index" refdomain="js" refexplicit="False" reftarget="a" reftype="meth" refwarn="False">
<literal classes="xref js js-meth">
a()
.
js:class (`sphinx.domains.javascript.JSConstructor`):
.
{js:class}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref js:module="True" js:object="True" refdoc="index" refdomain="js" refexplicit="False" reftarget="a" reftype="class" refwarn="False">
<literal classes="xref js js-class">
a()
.
js:data (`sphinx.domains.javascript.JSObject`):
.
{js:data}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref js:module="True" js:object="True" refdoc="index" refdomain="js" refexplicit="False" reftarget="a" reftype="data" refwarn="False">
<literal classes="xref js js-data">
a
.
js:attr (`sphinx.domains.javascript.JSXRefRole`):
.
{js:attr}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref js:module="True" js:object="True" refdoc="index" refdomain="js" refexplicit="False" reftarget="a" reftype="attr" refwarn="False">
<literal classes="xref js js-attr">
a
.
js:mod (`sphinx.domains.javascript.JSXRefRole`):
.
{js:mod}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref js:module="True" js:object="True" refdoc="index" refdomain="js" refexplicit="False" reftarget="a" reftype="mod" refwarn="False">
<literal classes="xref js js-mod">
a
.
eq (`sphinx.domains.math.MathReferenceRole`):
.
{eq}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="math" refexplicit="False" reftarget="a" reftype="eq" refwarn="True">
<literal classes="xref eq">
a
.
math:numref (`sphinx.domains.math.MathReferenceRole`):
.
{math:numref}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="math" refexplicit="False" reftarget="a" reftype="numref" refwarn="False">
<literal classes="xref math math-numref">
a
.
py:data (`sphinx.domains.python.PyVariable`):
.
{py:data}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="data" refwarn="False">
<literal classes="xref py py-data">
a
.
py:exc (`sphinx.domains.python.PyXRefRole`):
.
{py:exc}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="exc" refwarn="False">
<literal classes="xref py py-exc">
a
.
py:func (`sphinx.domains.python.PyXRefRole`):
.
{py:func}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="func" refwarn="False">
<literal classes="xref py py-func">
a()
.
py:class (`sphinx.domains.python.PyClasslike`):
.
{py:class}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="class" refwarn="False">
<literal classes="xref py py-class">
a
.
py:const (`sphinx.domains.python.PyXRefRole`):
.
{py:const}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="const" refwarn="False">
<literal classes="xref py py-const">
a
.
py:attr (`sphinx.domains.python.PyXRefRole`):
.
{py:attr}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="attr" refwarn="False">
<literal classes="xref py py-attr">
a
.
py:meth (`sphinx.domains.python.PyXRefRole`):
.
{py:meth}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="meth" refwarn="False">
<literal classes="xref py py-meth">
a()
.
py:mod (`sphinx.domains.python.PyXRefRole`):
.
{py:mod}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="mod" refwarn="False">
<literal classes="xref py py-mod">
a
.
py:obj (`sphinx.domains.python.PyXRefRole`):
.
{py:obj}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref py:class="True" py:module="True" refdoc="index" refdomain="py" refexplicit="False" reftarget="a" reftype="obj" refwarn="False">
<literal classes="xref py py-obj">
a
.
rst:role (`sphinx.domains.rst.ReSTRole`):
.
{rst:role}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="rst" refexplicit="False" reftarget="a" reftype="role" refwarn="False">
<literal classes="xref rst rst-role">
a
.
program (`sphinx.domains.std.Program`):
.
{program}`a`
.
<document source="<src>/index.md">
<paragraph>
<literal_strong classes="program">
a
.
option (`sphinx.domains.std.Cmdoption`):
.
{option}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="option" refwarn="True" std:program="True">
<literal classes="xref std std-option">
a
.
envvar (`sphinx.domains.std.EnvVarXRefRole`):
.
{envvar}`a`
.
<document source="<src>/index.md">
<paragraph>
<index entries="('single',\ 'a',\ 'index-0',\ '',\ None) ('single',\ 'environment\ variable;\ a',\ 'index-0',\ '',\ None)">
<target ids="index-0">
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="envvar" refwarn="False">
<literal classes="xref std std-envvar">
a
.
index (`sphinx.roles.Index`):
.
{index}`a`
.
<document source="<src>/index.md">
<paragraph>
<index entries="('single',\ 'a',\ 'index-0',\ '',\ None)">
<target ids="index-0">
a
.
download (`sphinx.roles.XRefRole`):
.
{download}`a`
.
<document source="<src>/index.md">
<paragraph>
<download_reference refdoc="index" refdomain="" refexplicit="False" reftarget="a" reftype="download" refwarn="False">
<literal classes="xref download">
a
.
any (`sphinx.roles.AnyXRefRole`):
.
{any}`a <alt text>`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="" refexplicit="True" reftarget="alt text" reftype="any" refwarn="True">
<literal classes="xref any">
a
.
pep (`sphinx.roles.PEP`):
.
{pep}`1`
.
<document source="<src>/index.md">
<paragraph>
<index entries="('single',\ 'Python\ Enhancement\ Proposals;\ PEP\ 1',\ 'index-0',\ '',\ None)">
<target ids="index-0">
<reference classes="pep" internal="False" refuri="https://peps.python.org/pep-0001/">
<strong>
PEP 1
.
rfc (`sphinx.roles.RFC`):
.
{rfc}`1`
.
<document source="<src>/index.md">
<paragraph>
<index entries="('single',\ 'RFC;\ RFC\ 1',\ 'index-0',\ '',\ None)">
<target ids="index-0">
<reference classes="rfc" internal="False" refuri="https://datatracker.ietf.org/doc/html/rfc1.html">
<strong>
RFC 1
.
guilabel (`sphinx.roles.GUILabel`):
.
{guilabel}`a`
.
<document source="<src>/index.md">
<paragraph>
<inline classes="guilabel" rawtext=":guilabel:`a`">
a
.
menuselection (`sphinx.roles.MenuSelection`):
.
{menuselection}`a`
.
<document source="<src>/index.md">
<paragraph>
<inline classes="menuselection" rawtext=":menuselection:`a`">
a
.
file (`sphinx.roles.EmphasizedLiteral`):
.
{file}`a`
.
<document source="<src>/index.md">
<paragraph>
<literal classes="file" role="file">
a
.
samp (`sphinx.roles.EmphasizedLiteral`):
.
{samp}`a`
.
<document source="<src>/index.md">
<paragraph>
<literal classes="samp" role="samp">
a
.
SKIP: Non-deterministic output
.
{abbr}`a`
.
<document source="notset">
<paragraph>
<abbreviation class="<function class_option at 0x1079fb830>>
a
.
rst:dir (`sphinx.roles.XRefRole`):
.
{rst:dir}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="rst" refexplicit="False" reftarget="a" reftype="dir" refwarn="False">
<literal classes="xref rst rst-dir">
a
.
token (`sphinx.roles.XRefRole`):
.
{token}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="token" refwarn="False">
<literal classes="xref std std-token">
a
.
term (`sphinx.roles.XRefRole`):
.
{term}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="term" refwarn="True">
<inline classes="xref std std-term">
a
.
ref (`sphinx.roles.XRefRole`):
.
{ref}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="ref" refwarn="True">
<inline classes="xref std std-ref">
a
.
ref with line breaks (`sphinx.roles.XRefRole`):
.
{ref}`some
text
<and
a
custom reference>`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="True" reftarget="and a custom reference" reftype="ref" refwarn="True">
<inline classes="xref std std-ref">
some text
.
numref (`sphinx.roles.XRefRole`):
.
{numref}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="numref" refwarn="True">
<literal classes="xref std std-numref">
a
.
keyword (`sphinx.roles.XRefRole`):
.
{keyword}`a`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="a" reftype="keyword" refwarn="True">
<literal classes="xref std std-keyword">
a
.
doc (`sphinx.roles.XRefRole`):
.
{doc}`this lecture <heavy_tails>`
.
<document source="<src>/index.md">
<paragraph>
<pending_xref refdoc="index" refdomain="std" refexplicit="True" reftarget="heavy_tails" reftype="doc" refwarn="True">
<inline classes="xref std std-doc">
this lecture
.

View File

@ -0,0 +1,800 @@
Raw
.
foo
.
<document source="<src>/index.md">
<paragraph>
foo
.
Hard-break
.
foo\
bar
.
<document source="<src>/index.md">
<paragraph>
foo
<raw format="html" xml:space="preserve">
<br />
<raw format="latex" xml:space="preserve">
\\
bar
.
Strong:
.
**foo**
.
<document source="<src>/index.md">
<paragraph>
<strong>
foo
.
Emphasis
.
*foo*
.
<document source="<src>/index.md">
<paragraph>
<emphasis>
foo
.
Escaped Emphasis:
.
\*foo*
.
<document source="<src>/index.md">
<paragraph>
*foo*
.
Mixed Inline
.
a *b* **c** `abc` \\*
.
<document source="<src>/index.md">
<paragraph>
a
<emphasis>
b
<strong>
c
<literal>
abc
\*
.
Inline Code:
.
`foo`
.
<document source="<src>/index.md">
<paragraph>
<literal>
foo
.
Heading:
.
# foo
.
<document source="<src>/index.md">
<section ids="foo" names="foo">
<title>
foo
.
Heading Levels:
.
# a
## b
### c
# d
.
<document source="<src>/index.md">
<section ids="a" names="a">
<title>
a
<section ids="b" names="b">
<title>
b
<section ids="c" names="c">
<title>
c
<section ids="d" names="d">
<title>
d
.
Block Code:
.
foo
.
<document source="<src>/index.md">
<literal_block language="none" xml:space="preserve">
foo
.
Fenced Code:
.
```sh
foo
```
.
<document source="<src>/index.md">
<literal_block language="sh" xml:space="preserve">
foo
.
Fenced Code no language:
.
```
foo
```
.
<document source="<src>/index.md">
<literal_block language="default" xml:space="preserve">
foo
.
Fenced Code no language with trailing whitespace:
.
```
foo
```
.
<document source="<src>/index.md">
<literal_block language="default" xml:space="preserve">
foo
.
Image empty:
.
![]()
.
<document source="<src>/index.md">
<paragraph>
<image alt="" uri="">
.
Image with alt and title:
.
![alt](src "title")
.
<document source="<src>/index.md">
<paragraph>
<image alt="alt" title="title" uri="src">
.
Image with escapable html:
.
![alt](http://www.google<>.com)
.
<document source="<src>/index.md">
<paragraph>
<image alt="alt" uri="http://www.google%3C%3E.com">
.
Block Quote:
.
> *foo*
.
<document source="<src>/index.md">
<block_quote>
<paragraph>
<emphasis>
foo
.
Bullet List:
.
- *foo*
* bar
.
<document source="<src>/index.md">
<bullet_list bullet="-">
<list_item>
<paragraph>
<emphasis>
foo
<bullet_list bullet="*">
<list_item>
<paragraph>
bar
.
Nested Bullets
.
- a
- b
- c
- d
.
<document source="<src>/index.md">
<bullet_list bullet="-">
<list_item>
<paragraph>
a
<bullet_list bullet="-">
<list_item>
<paragraph>
b
<bullet_list bullet="-">
<list_item>
<paragraph>
c
<list_item>
<paragraph>
d
.
Enumerated List:
.
1. *foo*
1) bar
para
10. starting
11. enumerator
.
<document source="<src>/index.md">
<enumerated_list enumtype="arabic" prefix="" suffix=".">
<list_item>
<paragraph>
<emphasis>
foo
<enumerated_list enumtype="arabic" prefix="" suffix=")">
<list_item>
<paragraph>
bar
<paragraph>
para
<enumerated_list enumtype="arabic" prefix="" start="10" suffix=".">
<list_item>
<paragraph>
starting
<list_item>
<paragraph>
enumerator
.
Nested Enumrated List:
.
1. a
2. b
1. c
.
<document source="<src>/index.md">
<enumerated_list enumtype="arabic" prefix="" suffix=".">
<list_item>
<paragraph>
a
<list_item>
<paragraph>
b
<enumerated_list enumtype="arabic" prefix="" suffix=".">
<list_item>
<paragraph>
c
.
Sphinx Role containing backtick:
.
{code}``a=1{`}``
.
<document source="<src>/index.md">
<paragraph>
<literal classes="code">
a=1{`}
.
Target:
.
(target)=
.
<document source="<src>/index.md">
<target ids="target" names="target">
.
Target with whitespace:
.
(target with space)=
.
<document source="<src>/index.md">
<target ids="target-with-space" names="target\ with\ space">
.
Referencing:
.
(target)=
Title
=====
[alt1](target)
[](target2)
[alt2](https://www.google.com)
[alt3](#target3)
.
<document source="<src>/index.md">
<target ids="target" names="target">
<section ids="title" names="title">
<title>
Title
<paragraph>
<pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="target" reftype="myst" refwarn="True">
<inline classes="xref myst">
alt1
<paragraph>
<pending_xref refdoc="index" refdomain="True" refexplicit="False" reftarget="target2" reftype="myst" refwarn="True">
<inline classes="xref myst">
<paragraph>
<reference refuri="https://www.google.com">
alt2
<paragraph>
<pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="#target3" reftype="myst" refwarn="True">
<inline classes="xref myst">
alt3
.
Comments:
.
line 1
% a comment
line 2
.
<document source="<src>/index.md">
<paragraph>
line 1
<comment xml:space="preserve">
a comment
<paragraph>
line 2
.
Block Break:
.
+++ string
.
<document source="<src>/index.md">
<comment classes="block_break" xml:space="preserve">
string
.
Link Reference:
.
[name][key]
[key]: https://www.google.com "a title"
.
<document source="<src>/index.md">
<paragraph>
<reference refuri="https://www.google.com" title="a title">
name
.
Link Reference short version:
.
[name]
[name]: https://www.google.com "a title"
.
<document source="<src>/index.md">
<paragraph>
<reference refuri="https://www.google.com" title="a title">
name
.
Block Quotes:
.
```{epigraph}
a b*c*
-- a**b**
```
.
<document source="<src>/index.md">
<block_quote classes="epigraph">
<paragraph>
a b
<emphasis>
c
<attribution>
a
<strong>
b
.
Link Definition in directive:
.
```{note}
[a]
```
[a]: link
.
<document source="<src>/index.md">
<note>
<paragraph>
<pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="link" reftype="myst" refwarn="True">
<inline classes="xref myst">
a
.
Link Definition in nested directives:
.
```{note}
[ref1]: link
```
```{note}
[ref1]
[ref2]
```
```{note}
[ref2]: link
```
.
<document source="<src>/index.md">
<note>
<note>
<paragraph>
<pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="link" reftype="myst" refwarn="True">
<inline classes="xref myst">
ref1
[ref2]
<note>
.
Footnotes:
.
[^a]
[^a]: footnote*text*
.
<document source="<src>/index.md">
<paragraph>
<footnote_reference auto="1" ids="id1" refname="a">
<transition classes="footnotes">
<footnote auto="1" ids="a" names="a">
<paragraph>
footnote
<emphasis>
text
.
Footnotes nested blocks:
.
[^a]
[^a]: footnote*text*
abc
xyz
> a
- b
c
finish
.
<document source="<src>/index.md">
<paragraph>
<footnote_reference auto="1" ids="id1" refname="a">
<paragraph>
finish
<transition classes="footnotes">
<footnote auto="1" ids="a" names="a">
<paragraph>
footnote
<emphasis>
text
<paragraph>
abc
xyz
<block_quote>
<paragraph>
a
<bullet_list bullet="-">
<list_item>
<paragraph>
b
<paragraph>
c
.
Front Matter:
.
---
a: 1
b: foo
c:
d: 2
---
.
<document source="<src>/index.md">
<field_list>
<field>
<field_name>
a
<field_body>
<paragraph>
<literal>
1
<field>
<field_name>
b
<field_body>
<paragraph>
<literal>
foo
<field>
<field_name>
c
<field_body>
<paragraph>
<literal>
{"d": 2}
.
Front Matter Biblio:
.
---
author: Chris Sewell
authors: Chris Sewell, Chris Hodgraf
organization: EPFL
address: |
1 Cedar Park Close
Thundersley
Essex
contact: <https://example.com>
version: 1.0
revision: 1.1
status: good
date: 2/12/1985
copyright: MIT
dedication: |
To my *homies*
abstract:
Something something **dark** side
other: Something else
---
.
<document source="<src>/index.md">
<field_list>
<field>
<field_name>
author
<field_body>
<paragraph>
Chris Sewell
<field>
<field_name>
authors
<field_body>
<paragraph>
Chris Sewell, Chris Hodgraf
<field>
<field_name>
organization
<field_body>
<paragraph>
EPFL
<field>
<field_name>
address
<field_body>
<paragraph>
1 Cedar Park Close
Thundersley
Essex
<field>
<field_name>
contact
<field_body>
<paragraph>
<reference refuri="https://example.com">
https://example.com
<field>
<field_name>
version
<field_body>
<paragraph>
1.0
<field>
<field_name>
revision
<field_body>
<paragraph>
1.1
<field>
<field_name>
status
<field_body>
<paragraph>
good
<field>
<field_name>
date
<field_body>
<paragraph>
2/12/1985
<field>
<field_name>
copyright
<field_body>
<paragraph>
MIT
<field>
<field_name>
dedication
<field_body>
<paragraph>
To my
<emphasis>
homies
<field>
<field_name>
abstract
<field_body>
<paragraph>
Something something
<strong>
dark
side
<field>
<field_name>
other
<field_body>
<paragraph>
<literal>
Something else
.
Front Matter Bad Yaml:
.
---
a: {
---
.
<document source="<src>/index.md">
<system_message level="2" line="1" source="<src>/index.md" type="WARNING">
<paragraph>
Malformed YAML [myst.topmatter]
.
Front Matter HTML Meta
.
---
myst:
html_meta:
keywords: Sphinx, documentation, builder
description lang=en: An amusing story
description lang=fr: Un histoire amusant
http-equiv=Content-Type: text/html; charset=ISO-8859-1
---
.
<document source="<src>/index.md">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="Sphinx, documentation, builder" name="keywords">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="An amusing story" lang="en" name="description">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="Un histoire amusant" lang="fr" name="description">
<pending>
.. internal attributes:
.transform: docutils.transforms.components.Filter
.details:
component: 'writer'
format: 'html'
nodes:
<meta content="text/html; charset=ISO-8859-1" http-equiv="Content-Type">
.
Full Test:
.
---
a: 1
---
(target)=
# header 1
## sub header 1
a *b* **c** `abc`
## sub header 2
x y [a](http://www.xyz.com) z
---
# header 2
```::python {a=1}
a = 1
```
[](target)
.
<document source="<src>/index.md">
<field_list>
<field>
<field_name>
a
<field_body>
<paragraph>
<literal>
1
<target ids="target" names="target">
<section ids="header-1" names="header\ 1">
<title>
header 1
<section ids="sub-header-1" names="sub\ header\ 1">
<title>
sub header 1
<paragraph>
a
<emphasis>
b
<strong>
c
<literal>
abc
<section ids="sub-header-2" names="sub\ header\ 2">
<title>
sub header 2
<paragraph>
x y
<reference refuri="http://www.xyz.com">
a
z
<transition>
<section ids="header-2" names="header\ 2">
<title>
header 2
<literal_block language="::python" xml:space="preserve">
a = 1
<paragraph>
<pending_xref refdoc="index" refdomain="True" refexplicit="False" reftarget="target" reftype="myst" refwarn="True">
<inline classes="xref myst">
.

View File

@ -0,0 +1,148 @@
Simple:
.
a|b
-|-
1|2
.
<document source="<src>/index.md">
<table classes="colwidths-auto">
<tgroup cols="2">
<colspec colwidth="50">
<colspec colwidth="50">
<thead>
<row>
<entry>
<paragraph>
a
<entry>
<paragraph>
b
<tbody>
<row>
<entry>
<paragraph>
1
<entry>
<paragraph>
2
.
Header only:
.
| abc | def |
| --- | --- |
.
<document source="<src>/index.md">
<table classes="colwidths-auto">
<tgroup cols="2">
<colspec colwidth="50">
<colspec colwidth="50">
<thead>
<row>
<entry>
<paragraph>
abc
<entry>
<paragraph>
def
.
Aligned:
.
a | b | c
:-|:-:| -:
1 | 2 | 3
.
<document source="<src>/index.md">
<table classes="colwidths-auto">
<tgroup cols="3">
<colspec colwidth="33">
<colspec colwidth="33">
<colspec colwidth="33">
<thead>
<row>
<entry classes="text-left">
<paragraph>
a
<entry classes="text-center">
<paragraph>
b
<entry classes="text-right">
<paragraph>
c
<tbody>
<row>
<entry classes="text-left">
<paragraph>
1
<entry classes="text-center">
<paragraph>
2
<entry classes="text-right">
<paragraph>
3
.
Nested syntax:
.
| *a* | __*b*__ |
| --- | -------- |
|c | {sub}`x` |
.
<document source="<src>/index.md">
<table classes="colwidths-auto">
<tgroup cols="2">
<colspec colwidth="50">
<colspec colwidth="50">
<thead>
<row>
<entry>
<paragraph>
<emphasis>
a
<entry>
<paragraph>
<strong>
<emphasis>
b
<tbody>
<row>
<entry>
<paragraph>
c
<entry>
<paragraph>
<subscript>
x
.
External links:
.
a|b
|-|-|
[link-a](https://www.google.com/)|[link-b](https://www.python.org/)
.
<document source="<src>/index.md">
<table classes="colwidths-auto">
<tgroup cols="2">
<colspec colwidth="50">
<colspec colwidth="50">
<thead>
<row>
<entry>
<paragraph>
a
<entry>
<paragraph>
b
<tbody>
<row>
<entry>
<paragraph>
<reference refuri="https://www.google.com/">
link-a
<entry>
<paragraph>
<reference refuri="https://www.python.org/">
link-b
.

View File

@ -0,0 +1,22 @@
"""Tests of the warning reporting for different MyST Markdown inputs."""
from io import StringIO
from pathlib import Path
import pytest
from docutils.core import publish_doctree
from myst_parser.parsers.docutils_ import Parser
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
@pytest.mark.param_file(FIXTURE_PATH / "reporter_warnings.md")
def test_basic(file_params):
"""Test basic functionality."""
report_stream = StringIO()
publish_doctree(
file_params.content,
parser=Parser(),
settings_overrides={"warning_stream": report_stream},
)
file_params.assert_expected(report_stream.getvalue(), rstrip=True)

View File

@ -0,0 +1,107 @@
"""Test fixture files, using the ``DocutilsRenderer``.
Note, the output AST is before any transforms are applied.
"""
import shlex
from io import StringIO
from pathlib import Path
import pytest
from docutils.core import Publisher, publish_doctree
from myst_parser.parsers.docutils_ import Parser
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
@pytest.mark.param_file(FIXTURE_PATH / "docutil_syntax_elements.md")
def test_syntax_elements(file_params, monkeypatch):
"""Test conversion of Markdown to docutils AST (before transforms are applied)."""
def _apply_transforms(self):
pass
monkeypatch.setattr(Publisher, "apply_transforms", _apply_transforms)
doctree = publish_doctree(
file_params.content,
source_path="notset",
parser=Parser(),
settings_overrides={"myst_highlight_code_blocks": False},
)
# in docutils 0.18 footnote ids have changed
outcome = doctree.pformat().replace('"footnote-reference-1"', '"id1"')
file_params.assert_expected(outcome, rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "docutil_roles.md")
def test_docutils_roles(file_params, monkeypatch):
"""Test conversion of Markdown to docutils AST (before transforms are applied)."""
def _apply_transforms(self):
pass
monkeypatch.setattr(Publisher, "apply_transforms", _apply_transforms)
doctree = publish_doctree(
file_params.content,
source_path="notset",
parser=Parser(),
)
ptree = doctree.pformat()
# docutils >=0.19 changes:
ptree = ptree.replace(
'refuri="http://tools.ietf.org/html/rfc1.html"',
'refuri="https://tools.ietf.org/html/rfc1.html"',
)
ptree = ptree.replace(
'refuri="http://www.python.org/dev/peps/pep-0000"',
'refuri="https://peps.python.org/pep-0000"',
)
file_params.assert_expected(ptree, rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "docutil_directives.md")
def test_docutils_directives(file_params, monkeypatch):
"""Test output of docutils directives."""
if "SKIP" in file_params.description: # line-block directive not yet supported
pytest.skip(file_params.description)
def _apply_transforms(self):
pass
monkeypatch.setattr(Publisher, "apply_transforms", _apply_transforms)
doctree = publish_doctree(
file_params.content,
source_path="notset",
parser=Parser(),
)
file_params.assert_expected(doctree.pformat(), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "docutil_syntax_extensions.txt")
def test_syntax_extensions(file_params):
"""The description is parsed as a docutils commandline"""
pub = Publisher(parser=Parser())
option_parser = pub.setup_option_parser()
try:
settings = option_parser.parse_args(
shlex.split(file_params.description)
).__dict__
except Exception as err:
raise AssertionError(
f"Failed to parse commandline: {file_params.description}\n{err}"
)
report_stream = StringIO()
settings["warning_stream"] = report_stream
doctree = publish_doctree(
file_params.content,
parser=Parser(),
settings_overrides=settings,
)
file_params.assert_expected(doctree.pformat(), rstrip_lines=True)

View File

@ -0,0 +1,119 @@
"""Test fixture files, using the ``SphinxRenderer``.
Note, the output AST is before any transforms are applied.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import pytest
from sphinx_pytest.plugin import CreateDoctree
from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
@pytest.mark.param_file(FIXTURE_PATH / "sphinx_syntax_elements.md")
def test_syntax_elements(file_params, sphinx_doctree_no_tr: CreateDoctree):
sphinx_doctree_no_tr.set_conf({"extensions": ["myst_parser"]})
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "tables.md")
def test_tables(file_params, sphinx_doctree_no_tr: CreateDoctree):
sphinx_doctree_no_tr.set_conf({"extensions": ["myst_parser"]})
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "directive_options.md")
def test_directive_options(file_params, sphinx_doctree_no_tr: CreateDoctree):
sphinx_doctree_no_tr.set_conf({"extensions": ["myst_parser"]})
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "sphinx_directives.md")
def test_sphinx_directives(file_params, sphinx_doctree_no_tr: CreateDoctree):
# TODO fix skipped directives
# TODO test domain directives
if file_params.title.startswith("SKIP") or file_params.title.startswith(
"SPHINX4-SKIP"
):
pytest.skip(file_params.title)
sphinx_doctree_no_tr.set_conf({"extensions": ["myst_parser"]})
pformat = sphinx_doctree_no_tr(file_params.content, "index.md").pformat("index")
# see https://github.com/sphinx-doc/sphinx/issues/9827
pformat = pformat.replace('<glossary sorted="False">', "<glossary>")
# see https://github.com/executablebooks/MyST-Parser/issues/522
if sys.maxsize == 2147483647:
pformat = pformat.replace('"2147483647"', '"9223372036854775807"')
file_params.assert_expected(pformat, rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "sphinx_roles.md")
def test_sphinx_roles(file_params, sphinx_doctree_no_tr: CreateDoctree):
if file_params.title.startswith("SKIP"):
pytest.skip(file_params.title)
sphinx_doctree_no_tr.set_conf({"extensions": ["myst_parser"]})
pformat = sphinx_doctree_no_tr(file_params.content, "index.md").pformat("index")
# sphinx 3 adds a parent key
pformat = re.sub('cpp:parent_key="[^"]*"', 'cpp:parent_key=""', pformat)
# sphinx >= 4.5.0 adds a trailing slash to PEP URLs,
# see https://github.com/sphinx-doc/sphinx/commit/658689433eacc9eb
pformat = pformat.replace(
' refuri="http://www.python.org/dev/peps/pep-0001">',
' refuri="http://www.python.org/dev/peps/pep-0001/">',
)
file_params.assert_expected(pformat, rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "dollarmath.md")
def test_dollarmath(file_params, sphinx_doctree_no_tr: CreateDoctree):
sphinx_doctree_no_tr.set_conf(
{"extensions": ["myst_parser"], "myst_enable_extensions": ["dollarmath"]}
)
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "amsmath.md")
def test_amsmath(file_params, sphinx_doctree_no_tr: CreateDoctree, monkeypatch):
monkeypatch.setattr(SphinxRenderer, "_random_label", lambda self: "mock-uuid")
sphinx_doctree_no_tr.set_conf(
{"extensions": ["myst_parser"], "myst_enable_extensions": ["amsmath"]}
)
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "containers.md")
def test_containers(file_params, sphinx_doctree_no_tr: CreateDoctree, monkeypatch):
monkeypatch.setattr(SphinxRenderer, "_random_label", lambda self: "mock-uuid")
sphinx_doctree_no_tr.set_conf(
{"extensions": ["myst_parser"], "myst_enable_extensions": ["colon_fence"]}
)
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "eval_rst.md")
def test_evalrst_elements(file_params, sphinx_doctree_no_tr: CreateDoctree):
sphinx_doctree_no_tr.set_conf({"extensions": ["myst_parser"]})
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
@pytest.mark.param_file(FIXTURE_PATH / "definition_lists.md")
def test_definition_lists(file_params, sphinx_doctree_no_tr: CreateDoctree):
sphinx_doctree_no_tr.set_conf(
{"extensions": ["myst_parser"], "myst_enable_extensions": ["deflist"]}
)
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(result.pformat("index"), rstrip_lines=True)

View File

@ -0,0 +1,52 @@
import os
from io import StringIO
from pathlib import Path
import pytest
from docutils.core import publish_doctree
from myst_parser.parsers.docutils_ import Parser
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
@pytest.mark.param_file(FIXTURE_PATH / "mock_include.md")
def test_render(file_params, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
tmp_path.joinpath("other.md").write_text("a\nb\nc")
tmp_path.joinpath("fmatter.md").write_text("---\na: 1\n---\nb")
doctree = publish_doctree(
file_params.content,
parser=Parser(),
settings_overrides={"myst_highlight_code_blocks": False},
)
doctree["source"] = "tmpdir/test.md"
output = doctree.pformat().replace(str(tmp_path) + os.sep, "tmpdir" + "/").rstrip()
file_params.assert_expected(output, rstrip=True)
@pytest.mark.param_file(FIXTURE_PATH / "mock_include_errors.md")
def test_errors(file_params, tmp_path, monkeypatch):
if file_params.title.startswith("Non-existent path") and os.name == "nt":
pytest.skip("tmp_path not converted correctly on Windows")
monkeypatch.chdir(tmp_path)
tmp_path.joinpath("bad.md").write_text("{a}`b`")
report_stream = StringIO()
publish_doctree(
file_params.content,
source_path=str(tmp_path / "test.md"),
parser=Parser(),
settings_overrides={"halt_level": 6, "warning_stream": report_stream},
)
file_params.assert_expected(
report_stream.getvalue().replace(str(tmp_path) + os.sep, "tmpdir" + "/"),
rstrip=True,
)

View File

@ -0,0 +1,38 @@
"""Test (docutils) parsing with different ``MdParserConfig`` options set."""
import shlex
from io import StringIO
from pathlib import Path
import pytest
from docutils.core import Publisher, publish_doctree
from myst_parser.parsers.docutils_ import Parser
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
@pytest.mark.param_file(FIXTURE_PATH / "myst-config.txt")
def test_cmdline(file_params):
"""The description is parsed as a docutils commandline"""
pub = Publisher(parser=Parser())
option_parser = pub.setup_option_parser()
try:
settings = option_parser.parse_args(
shlex.split(file_params.description)
).__dict__
except Exception as err:
raise AssertionError(
f"Failed to parse commandline: {file_params.description}\n{err}"
)
report_stream = StringIO()
settings["warning_stream"] = report_stream
doctree = publish_doctree(
file_params.content,
parser=Parser(),
settings_overrides=settings,
)
output = doctree.pformat()
warnings = report_stream.getvalue()
if warnings:
output += "\n" + warnings
file_params.assert_expected(output, rstrip_lines=True)

View File

@ -0,0 +1,38 @@
import pytest
from sphinx_pytest.plugin import CreateDoctree
@pytest.mark.parametrize(
"test_name,text,should_warn",
[
("null", "", False),
("missing", "[](ref)", True),
("doc", "[](index)", False),
("doc_with_extension", "[](index.md)", False),
("doc_nested", "[*text*](index)", False),
("ref", "(ref)=\n# Title\n[](ref)", False),
("ref_nested", "(ref)=\n# Title\n[*text*](ref)", False),
("duplicate", "(index)=\n# Title\n[](index)", True),
("ref_colon", "(ref:colon)=\n# Title\n[](ref:colon)", False),
],
)
def test_parse(
test_name: str,
text: str,
should_warn: bool,
sphinx_doctree: CreateDoctree,
file_regression,
):
sphinx_doctree.set_conf({"extensions": ["myst_parser"]})
result = sphinx_doctree(text, "index.md")
assert not result.warnings
doctree = result.get_resolved_doctree("index")
if should_warn:
assert result.warnings
else:
assert not result.warnings
doctree["source"] = "root/index.md"
file_regression.check(doctree.pformat(), basename=test_name, extension=".xml")

View File

@ -0,0 +1,5 @@
<document source="root/index.md">
<paragraph>
<reference internal="True" refuri="">
<inline classes="doc std std-doc">
<no title>

Some files were not shown because too many files have changed in this diff Show More