From 7dda7b592a7b0f727137f1421dfb60b010c20a21 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 20 Apr 2022 11:06:35 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20Do=20not=20let=20?= =?UTF-8?q?sphinx=20check=20the=20config=20type=20(#559)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is already validated by myst-parser --- myst_parser/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/myst_parser/__init__.py b/myst_parser/__init__.py index e51e10f7..2be73019 100644 --- a/myst_parser/__init__.py +++ b/myst_parser/__init__.py @@ -1,5 +1,5 @@ """An extended commonmark compliant parser, with bridges to docutils & sphinx.""" -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any __version__ = "0.17.2" @@ -37,7 +37,7 @@ def setup_sphinx(app: "Sphinx"): 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") + app.add_config_value(f"myst_{name}", default, "env", types=Any) app.connect("builder-inited", create_myst_config) app.connect("builder-inited", override_mathjax) From 602470ebdaf81fbea999fcc0f0cf1b8e7784ec15 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 12 May 2022 15:09:37 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=F0=9F=93=9A=20Restructur?= =?UTF-8?q?e=20code=20base=20and=20documentation=20(#566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR restructures the code base into modules, to be more coherent as to the purpose of each module. It also restructures the documentation, to make it easier for users to follow, and expose the core concerns at the top-level. Finally, it introduces document-level configuration *via* the Markdown top-matter, under the `myst` key. This configuration is generated from the code `MdParserConfig` dataclass, so is inherently consistent with global configuration. The (YAML) top-matter of a MyST file is always read (within the docutils/sphinx parsers) before the full document is parsed, in order to acquire this file configuration, which overrides the default/global configuration (see `docs/configuration.md`). ## Breaking changes This should not be breaking, for general users of the sphinx extension, 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. These were really just for testing, and were confusing for users, since they did not run the full sphinx build. Instead, for single page builds, users should use the recently added 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** --- ``` --- .github/workflows/docutils_setup.py | 4 +- .github/workflows/tests.yml | 4 +- .pre-commit-config.yaml | 8 +- .readthedocs.yml | 2 +- docs/_static/custom.css | 27 +- docs/api/index.md | 14 - docs/api/parsers.md | 301 ----------- docs/api/reference.rst | 100 ++-- docs/api/renderers.md | 146 ------ docs/conf.py | 96 +--- docs/configuration.md | 106 ++++ .../index.md => develop/background.md} | 2 +- docs/develop/index.md | 2 +- docs/docutils.md | 2 +- docs/examples/heavy_tails.md | 5 - docs/examples/htop_again.png | Bin 99091 -> 0 bytes docs/examples/index.md | 32 -- docs/examples/kesten_processes.md | 4 - docs/examples/references.bib | 12 - docs/examples/wealth_dynamics_md.md | 482 ----------------- docs/examples/wealth_dynamics_rst.rst | 493 ------------------ docs/{sphinx/use.md => faq/index.md} | 65 ++- docs/{sphinx => faq}/snippets/include-md.md | 0 docs/{sphinx => faq}/snippets/include-rst.rst | 0 docs/index.md | 181 ++++--- docs/intro.md | 250 +++++++++ docs/sphinx/faq.md | 16 - docs/sphinx/index.md | 17 - docs/sphinx/intro.md | 423 --------------- docs/sphinx/reference.md | 129 ----- docs/sphinx/roles-and-directives.md | 23 - docs/syntax/optional.md | 76 +-- docs/syntax/reference.md | 7 +- docs/syntax/roles-and-directives.md | 421 +++++++++++++++ docs/syntax/syntax.md | 452 +++------------- example-include.md | 2 +- myst_parser/__init__.py | 63 +-- myst_parser/_docs.py | 198 +++++++ myst_parser/cli.py | 3 +- myst_parser/config/__init__.py | 1 + myst_parser/{ => config}/dc_validators.py | 73 ++- myst_parser/config/main.py | 408 +++++++++++++++ myst_parser/docutils_.py | 264 +--------- myst_parser/main.py | 442 ---------------- myst_parser/mdit_to_docutils/__init__.py | 1 + .../base.py} | 244 ++++----- .../{ => mdit_to_docutils}/html_to_nodes.py | 14 +- .../sphinx_.py} | 183 ++----- myst_parser/{ => mdit_to_docutils}/utils.py | 0 myst_parser/mocking.py | 44 +- myst_parser/parsers/__init__.py | 1 + .../directives.py} | 16 +- myst_parser/parsers/docutils_.py | 275 ++++++++++ myst_parser/parsers/mdit.py | 120 +++++ myst_parser/{ => parsers}/parse_html.py | 58 +-- myst_parser/parsers/sphinx_.py | 69 +++ myst_parser/sphinx_.py | 2 +- myst_parser/sphinx_ext/__init__.py | 1 + myst_parser/{ => sphinx_ext}/directives.py | 2 +- myst_parser/sphinx_ext/main.py | 60 +++ myst_parser/{ => sphinx_ext}/mathjax.py | 6 +- myst_parser/{ => sphinx_ext}/myst_refs.py | 4 + myst_parser/sphinx_parser.py | 80 --- pyproject.toml | 19 +- tests/conftest.py | 8 - tests/test_commonmark/test_commonmark.py | 7 +- tests/test_docutils.py | 4 +- tests/test_html/test_html_to_nodes.py | 4 +- tests/test_html/test_parse_html.py | 2 +- tests/test_renderers/fixtures/amsmath.md | 18 +- tests/test_renderers/fixtures/containers.md | 8 +- .../fixtures/definition_lists.md | 3 +- .../fixtures/directive_options.md | 57 +- .../fixtures/docutil_syntax_elements.md | 61 +-- tests/test_renderers/fixtures/dollarmath.md | 22 +- tests/test_renderers/fixtures/eval_rst.md | 4 +- .../fixtures/reporter_warnings.md | 39 +- .../fixtures/sphinx_directives.md | 104 ++-- tests/test_renderers/fixtures/sphinx_roles.md | 262 ++++------ .../fixtures/sphinx_syntax_elements.md | 163 +++--- tests/test_renderers/fixtures/tables.md | 17 +- tests/test_renderers/test_error_reporting.py | 2 +- .../test_renderers/test_fixtures_docutils.py | 65 ++- tests/test_renderers/test_fixtures_sphinx.py | 108 ++-- .../test_renderers/test_include_directive.py | 52 +- tests/test_renderers/test_myst_config.py | 2 +- tests/test_renderers/test_myst_refs.py | 44 +- .../test_myst_refs/duplicate.xml | 14 +- .../test_renderers/test_myst_refs/missing.xml | 3 +- tests/test_renderers/test_myst_refs/ref.xml | 15 +- .../test_myst_refs/ref_colon.xml | 15 +- .../test_myst_refs/ref_nested.xml | 17 +- tests/test_renderers/test_parse_directives.py | 2 +- tests/test_sphinx/conftest.py | 2 +- .../sourcedirs/substitutions/index.md | 37 +- tests/test_sphinx/test_sphinx_builds.py | 2 +- 96 files changed, 3077 insertions(+), 4641 deletions(-) delete mode 100644 docs/api/index.md delete mode 100644 docs/api/parsers.md delete mode 100644 docs/api/renderers.md create mode 100644 docs/configuration.md rename docs/{explain/index.md => develop/background.md} (99%) delete mode 100644 docs/examples/heavy_tails.md delete mode 100644 docs/examples/htop_again.png delete mode 100644 docs/examples/index.md delete mode 100644 docs/examples/kesten_processes.md delete mode 100644 docs/examples/references.bib delete mode 100644 docs/examples/wealth_dynamics_md.md delete mode 100644 docs/examples/wealth_dynamics_rst.rst rename docs/{sphinx/use.md => faq/index.md} (78%) rename docs/{sphinx => faq}/snippets/include-md.md (100%) rename docs/{sphinx => faq}/snippets/include-rst.rst (100%) create mode 100644 docs/intro.md delete mode 100644 docs/sphinx/faq.md delete mode 100644 docs/sphinx/index.md delete mode 100644 docs/sphinx/intro.md delete mode 100644 docs/sphinx/reference.md delete mode 100644 docs/sphinx/roles-and-directives.md create mode 100644 docs/syntax/roles-and-directives.md create mode 100644 myst_parser/_docs.py create mode 100644 myst_parser/config/__init__.py rename myst_parser/{ => config}/dc_validators.py (59%) create mode 100644 myst_parser/config/main.py delete mode 100644 myst_parser/main.py create mode 100644 myst_parser/mdit_to_docutils/__init__.py rename myst_parser/{docutils_renderer.py => mdit_to_docutils/base.py} (91%) rename myst_parser/{ => mdit_to_docutils}/html_to_nodes.py (92%) rename myst_parser/{sphinx_renderer.py => mdit_to_docutils/sphinx_.py} (58%) rename myst_parser/{ => mdit_to_docutils}/utils.py (100%) create mode 100644 myst_parser/parsers/__init__.py rename myst_parser/{parse_directives.py => parsers/directives.py} (94%) create mode 100644 myst_parser/parsers/docutils_.py create mode 100644 myst_parser/parsers/mdit.py rename myst_parser/{ => parsers}/parse_html.py (89%) create mode 100644 myst_parser/parsers/sphinx_.py create mode 100644 myst_parser/sphinx_ext/__init__.py rename myst_parser/{ => sphinx_ext}/directives.py (98%) create mode 100644 myst_parser/sphinx_ext/main.py rename myst_parser/{ => sphinx_ext}/mathjax.py (97%) rename myst_parser/{ => sphinx_ext}/myst_refs.py (99%) delete mode 100644 myst_parser/sphinx_parser.py delete mode 100644 tests/conftest.py diff --git a/.github/workflows/docutils_setup.py b/.github/workflows/docutils_setup.py index a9f8f2fa..9d745222 100755 --- a/.github/workflows/docutils_setup.py +++ b/.github/workflows/docutils_setup.py @@ -44,12 +44,12 @@ def modify_readme(content: str) -> str: if __name__ == "__main__": project_path = sys.argv[1] readme_path = sys.argv[2] - with open(project_path, "r") as f: + 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, "r") as f: + with open(readme_path) as f: content = f.read() content = modify_readme(content) with open(readme_path, "w") as f: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20d067a9..4bbe10c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,7 +89,7 @@ jobs: - name: Install dependencies run: | pip install . - pip install pytest~=6.2 pytest-param-files~=0.3.3 docutils==${{ matrix.docutils-version }} + 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 "\ @@ -100,7 +100,7 @@ jobs: else: raise AssertionError()" - name: Run pytest for docutils-only tests - run: pytest tests/test_docutils.py tests/test_renderers/test_fixtures_docutils.py + 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 589cd3e0..a0ca0fc9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,12 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace + - repo: https://github.com/asottile/pyupgrade + rev: v2.32.0 + hooks: + - id: pyupgrade + args: [--py37-plus] + - repo: https://github.com/PyCQA/isort rev: 5.10.1 hooks: @@ -44,7 +50,7 @@ repos: - id: mypy args: [--config-file=pyproject.toml] additional_dependencies: - - sphinx~=3.3 + - sphinx~=4.1 - markdown-it-py>=1.0.0,<3.0.0 - mdit-py-plugins~=0.3.0 files: > diff --git a/.readthedocs.yml b/.readthedocs.yml index cb2ff6ae..43b85263 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,7 @@ version: 2 python: - version: 3 + version: "3" install: - method: pip path: . diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 118c5a83..f126fba7 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,11 +1,24 @@ -.bg-myst-one { - background-color: #52d16f3b; +/** Add a counter before subsections **/ +h1 { + counter-reset: subsection; + text-decoration: underline; } - -.bg-myst-two { - background-color: #e7dd7b73; +h2 { + counter-reset: subsubsection; +} +h2::before { + counter-increment: subsection; + content: counter(subsection) ". "; +} +h3::before { + counter-increment: subsubsection; + content: counter(subsection) "." counter(subsubsection) ". "; } -.bg-myst-three { - background-color: #e7b07b96; +/** 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; } diff --git a/docs/api/index.md b/docs/api/index.md deleted file mode 100644 index c646a562..00000000 --- a/docs/api/index.md +++ /dev/null @@ -1,14 +0,0 @@ -(api/main)= - -# MyST-Parser Python API - -MyST-Parser also has a Python API *via* the `myst_parser` package. -This allows you to parse MyST Markdown and render various outputs with Python. - -```{toctree} -:maxdepth: 2 - -parsers.md -renderers.md -reference.rst -``` diff --git a/docs/api/parsers.md b/docs/api/parsers.md deleted file mode 100644 index b675db2c..00000000 --- a/docs/api/parsers.md +++ /dev/null @@ -1,301 +0,0 @@ -# Parse MyST Markdown - -```{seealso} -- The MyST Parser package heavily uses the the [markdown-it-py](https://github.com/executablebooks/markdown-it-py) package. -- {ref}`The MyST-Parser API reference ` contains a more complete reference of this API. -``` - -## Parsing and rendering helper functions - -The MyST Parser comes bundled with some helper functions to quickly parse MyST Markdown and render its output. - -:::{important} -These APIs are primarily intended for testing and development purposes. -For proper parsing see {ref}`myst-sphinx` and {ref}`myst-docutils`. -::: - -### Parse MyST Markdown to HTML - -The following code parses markdown and renders as HTML using only the markdown-it parser -(i.e. no sphinx or docutils specific processing is done): - -```python -from myst_parser.main import to_html -to_html("some *text* {literal}`a`") -``` - - -```html -'

some text {literal}[a]

\n' -``` - - -### Parse MyST Markdown to docutils - -The following function renders your text as **docutils AST objects** (for example, for use with the Sphinx ecosystem): - -```python -from myst_parser.main import to_docutils -print(to_docutils("some *text* {literal}`a`").pformat()) -``` - -```xml - - - some - - text - - - a -``` - -:::{note} -This function only performs the initial parse of the AST, -without applying any transforms or post-processing. -See for example the [Sphinx core events](https://www.sphinx-doc.org/en/master/extdev/appapi.html?highlight=config-inited#sphinx-core-events). -::: - -### Parse MyST Markdown as `markdown-it` tokens - -The MyST Parser uses `markdown-it-py` tokens as an intermediate representation of your text. -Normally these tokens are then *rendered* into various outputs. -If you'd like direct access to the tokens, use the `to_tokens` function. -Here's an example of its use: - -```python -from pprint import pprint -from myst_parser.main import to_tokens - -for token in to_tokens("some *text*"): - print(token, "\n") -``` - - -```python -Token(type='paragraph_open', tag='p', nesting=1, attrs=None, map=[0, 1], level=0, children=None, content='', markup='', info='', meta={}, block=True, hidden=False) - -Token(type='inline', tag='', nesting=0, attrs=None, map=[0, 1], level=1, children=[Token(type='text', tag='', nesting=0, attrs=None, map=None, level=0, children=None, content='some ', markup='', info='', meta={}, block=False, hidden=False), Token(type='em_open', tag='em', nesting=1, attrs=None, map=None, level=0, children=None, content='', markup='*', info='', meta={}, block=False, hidden=False), Token(type='text', tag='', nesting=0, attrs=None, map=None, level=1, children=None, content='text', markup='', info='', meta={}, block=False, hidden=False), Token(type='em_close', tag='em', nesting=-1, attrs=None, map=None, level=0, children=None, content='', markup='*', info='', meta={}, block=False, hidden=False)], content='some *text*', markup='', info='', meta={}, block=True, hidden=False) - -Token(type='paragraph_close', tag='p', nesting=-1, attrs=None, map=None, level=0, children=None, content='', markup='', info='', meta={}, block=True, hidden=False) -``` - - -Each token is an abstract representation of a piece of MyST Markdown syntax. - -## Use the parser object for more control - -The MyST Parser is actually a `markdown-it-py` parser with several extensions pre-enabled that support the MyST syntax. -If you'd like more control over the parsing process, then you can directly use a `markdown-it-py` parser with MyST syntax extensions loaded. - -:::{seealso} -[`markdown-it-py`](https://markdown-it-py.readthedocs.io/) is an extensible Python parser and renderer for flavors of markdown. -It is inspired heavily by the [`markdown-it`](https://github.com/markdown-it/markdown-it) Javascript package. -See the documentation of these tools for more information. -::: - -### Load a parser - -To load one of these parsers for your own use, use the `create_md_parser` function. -Below we'll create such a parser and show that it is an instance of a `markdown-it-py` parser: - -```python -from markdown_it.renderer import RendererHTML -from myst_parser.main import create_md_parser, MdParserConfig -config = MdParserConfig() -parser = create_md_parser(config, RendererHTML) -parser -``` - - -```python -markdown_it.main.MarkdownIt() -``` - - -### List the active rules - -We can list the **currently active rules** for this parser. -Each rules maps onto a particular markdown syntax, and a Token. -To list the active rules, use the `get_active_rules` method: - -```python -pprint(parser.get_active_rules()) -``` - - -```python -{'block': ['front_matter', - 'table', - 'code', - 'math_block_label', - 'math_block', - 'fence', - 'myst_line_comment', - 'blockquote', - 'myst_block_break', - 'myst_target', - 'hr', - 'list', - 'footnote_def', - 'reference', - 'heading', - 'lheading', - 'html_block', - 'paragraph'], - 'core': ['normalize', 'block', 'inline'], - 'inline': ['text', - 'newline', - 'math_inline', - 'math_single', - 'escape', - 'myst_role', - 'backticks', - 'emphasis', - 'link', - 'image', - 'footnote_ref', - 'autolink', - 'html_inline', - 'entity'], - 'inline2': ['balance_pairs', 'emphasis', 'text_collapse']} -``` - - -### Parse and render markdown - -Once we have a Parser instance, we can use it to parse some markdown. -Use the `render` function to do so: - -```python -parser.render("*abc*") -``` - - -```html -'

abc

\n' -``` - - -### Disable and enable rules - -You can disable and enable rules for a parser using the `disable` and `enable` methods. -For example, below we'll disable the `emphasis` rule (which is what detected the `*abc*` syntax above) and re-render the text: - -```python -parser.disable("emphasis").render("*abc*") -``` - - -```html -'

*abc*

\n' -``` - - -As you can see, the parser no longer detected the `**` syntax as requiring an _emphasis_. - -### Turn off all block-level syntax - -If you'd like to use your parser *only* for in-line content, you may turn off all block-level syntax with the `renderInline` method: - -```python -parser.enable("emphasis").renderInline("- *abc*") -``` - - -```html -'- abc' -``` - - - -## The Token Stream - -When you parse markdown with the MyST Parser, the result is a flat stream of **Tokens**. -These are abstract representations of each type of syntax that the parser has detected. - -For example, below we'll show the token stream for some simple markdown: - -```python -from myst_parser.main import to_tokens -tokens = to_tokens(""" -Here's some *text* - -1. a list - -> a *quote*""") -[t.type for t in tokens] -``` - - -```python -['paragraph_open', - 'inline', - 'paragraph_close', - 'ordered_list_open', - 'list_item_open', - 'paragraph_open', - 'inline', - 'paragraph_close', - 'list_item_close', - 'ordered_list_close', - 'blockquote_open', - 'paragraph_open', - 'inline', - 'paragraph_close', - 'blockquote_close'] -``` - - -Note that these tokens are **flat**, although some of the tokens refer to one another (for example, Tokens with `_open` and `_close` represent the start/end of blocks). - -Tokens of type `inline` will have a `children` attribute that contains a list of the Tokens that they contain. -For example: - -```python -tokens[6] -``` - - -```python -Token(type='inline', tag='', nesting=0, attrs=None, map=[3, 4], level=3, children=[Token(type='text', tag='', nesting=0, attrs=None, map=None, level=0, children=None, content='a list', markup='', info='', meta={}, block=False, hidden=False)], content='a list', markup='', info='', meta={}, block=True, hidden=False) -``` - - -### Rendering tokens - -The list of Token objects can be rendered to a number of different outputs. -This involves first processing the Tokens, and then defining how each should be rendered in an output format (e.g., HTML or Docutils). - -For example, the sphinx renderer first converts the token to a nested structure, collapsing the opening/closing tokens into single tokens: - -```python -from markdown_it.token import nest_tokens -nested = nest_tokens(tokens) -[t.type for t in nested] -``` - - -```python -['paragraph_open', 'ordered_list_open', 'blockquote_open'] -``` - - -```python -print(nested[0].opening, end="\n\n") -print(nested[0].closing, end="\n\n") -print(nested[0].children, end="\n\n") -``` - - -```python -Token(type='paragraph_open', tag='p', nesting=1, attrs=None, map=[1, 2], level=0, children=None, content='', markup='', info='', meta={}, block=True, hidden=False) - -Token(type='paragraph_close', tag='p', nesting=-1, attrs=None, map=None, level=0, children=None, content='', markup='', info='', meta={}, block=True, hidden=False) - -[Token(type='inline', tag='', nesting=0, attrs=None, map=[1, 2], level=1, children=[Token(type='text', tag='', nesting=0, attrs=None, map=None, level=0, children=None, content="Here's some ", markup='', info='', meta={}, block=False, hidden=False), NestedTokens(opening=Token(type='em_open', tag='em', nesting=1, attrs=None, map=None, level=0, children=None, content='', markup='*', info='', meta={}, block=False, hidden=False), closing=Token(type='em_close', tag='em', nesting=-1, attrs=None, map=None, level=0, children=None, content='', markup='*', info='', meta={}, block=False, hidden=False), children=[Token(type='text', tag='', nesting=0, attrs=None, map=None, level=1, children=None, content='text', markup='', info='', meta={}, block=False, hidden=False)])], content="Here's some *text*", markup='', info='', meta={}, block=True, hidden=False)] -``` - - -It then renders each token to a Sphinx-based docutils object. -See [the renderers section](renderers.md) for more information about rendering tokens. diff --git a/docs/api/reference.rst b/docs/api/reference.rst index ffd17dd6..9ea58983 100644 --- a/docs/api/reference.rst +++ b/docs/api/reference.rst @@ -1,29 +1,49 @@ -============= -API Reference -============= +.. _api/main: -.. _api/directive: +========== +Python API +========== -Directive Parsing Reference ---------------------------- +Source text parsers +------------------- -.. automodule:: myst_parser.parse_directives - :members: +.. _api/docutils_parser: + +Docutils +........ + +.. autoclass:: myst_parser.docutils_.Parser + :members: parse + :undoc-members: + :member-order: bysource + :show-inheritance: + +.. _api/sphinx_parser: + +Sphinx +...... -MyST Renderers --------------- +.. 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. +with some additional methods only available *via* sphinx e.g. multi-document cross-referencing. Docutils ........ -.. autoclass:: myst_parser.docutils_renderer.DocutilsRenderer +.. 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: @@ -34,15 +54,22 @@ Docutils Sphinx ...... -.. autoclass:: myst_parser.sphinx_renderer.SphinxRenderer +.. 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: -Mocking -....... +.. _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, @@ -67,44 +94,3 @@ but instead run nested parsing with the markdown parser. :members: :undoc-members: :show-inheritance: - - -Additional Methods -.................. - -.. autofunction:: myst_parser.docutils_renderer.make_document - -.. autofunction:: myst_parser.docutils_renderer.html_meta_to_nodes - -.. autofunction:: myst_parser.sphinx_renderer.minimal_sphinx_app - -.. autofunction:: myst_parser.sphinx_renderer.mock_sphinx_env - - -.. _api/docutils_parser: - -Docutils Parser Reference -------------------------- - -.. autoclass:: myst_parser.docutils_.Parser - :members: parse - :undoc-members: - :member-order: bysource - :show-inheritance: - -.. _api/sphinx_parser: - -Sphinx Parser Reference ------------------------ - -This class builds on the :py:class:`~myst_parser.sphinx_renderer.SphinxRenderer` -to generate a parser for Sphinx, using the :ref:`Sphinx parser API `: - -.. autoclass:: myst_parser.sphinx_parser.MystParser - :members: supported, parse - :undoc-members: - :member-order: bysource - :show-inheritance: - :exclude-members: __init__ - -.. _api/renderers: diff --git a/docs/api/renderers.md b/docs/api/renderers.md deleted file mode 100644 index bdc9d60f..00000000 --- a/docs/api/renderers.md +++ /dev/null @@ -1,146 +0,0 @@ -# Render outputs - -There are a few different ways to render MyST Parser tokens into different outputs. -This section covers a few common ones. - -## The `docutils` renderer - -The `myst_parser.docutils_renderer.DocutilsRenderer` converts a token directly to the `docutils.document` representation of the document, converting roles and directives to a `docutils.nodes` if a converter can be found for the given name. - -````python -from myst_parser.main import to_docutils - -document = to_docutils(""" -Here's some *text* - -1. a list - -> a quote - -{emphasis}`content` - -```{sidebar} my sidebar -content -``` -""") - -print(document.pformat()) -```` - -```xml - - - Here's some - - text - - - - a list - - - a quote - - - content - - - my sidebar - <paragraph> - content -``` - -## The Sphinx renderer - -The `myst_parser.sphinx_renderer.SphinxRenderer` builds on the `DocutilsRenderer` to add sphinx specific nodes, e.g. for cross-referencing between documents. - -To use the sphinx specific roles and directives outside of a `sphinx-build`, they must first be loaded with the `in_sphinx_env` option. - -````python -document = to_docutils(""" -Here's some *text* - -1. a list - -> a quote - -{ref}`target` - -```{glossary} my gloassary -name - definition -``` -""", - in_sphinx_env=True) -print(document.pformat()) -```` - -```xml -<document source="notset"> - <paragraph> - Here's some - <emphasis> - text - <enumerated_list> - <list_item> - <paragraph> - a list - <block_quote> - <paragraph> - a quote - <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="target" reftype="ref" refwarn="True"> - <inline classes="xref std std-ref"> - target - <glossary> - <definition_list classes="glossary"> - <definition_list_item> - <term ids="term-my-gloassary"> - my gloassary - <index entries="('single',\ 'my\ gloassary',\ 'term-my-gloassary',\ 'main',\ None)"> - <term ids="term-name"> - name - <index entries="('single',\ 'name',\ 'term-name',\ 'main',\ None)"> - <definition> - <paragraph> - definition -``` - -### Set Sphinx configuration for testing - -You can also set Sphinx configuration *via* `sphinx_conf`. This is a dictionary representation of the contents of the Sphinx `conf.py`. - -```{warning} -This feature is only meant for simple testing. -It will fail for extensions that require the full -Sphinx build process and/or access to external files. -``` - -`````python -document = to_docutils(""" -````{tabs} - -```{tab} Apples - -Apples are green, or sometimes red. -``` -```` -""", - in_sphinx_env=True, - conf={"extensions": ["sphinx_tabs.tabs"]} -) -print(document.pformat()) -````` - -```xml -<document source="notset"> - <container classes="sphinx-tabs"> - <container> - <a classes="item"> - <container> - <paragraph> - Apples - <container classes="ui bottom attached sphinx-tab tab segment sphinx-data-tab-0-0 active"> - <paragraph> - Apples are green, or sometimes red. -``` diff --git a/docs/conf.py b/docs/conf.py index 34f5034c..e2d0c5dc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,15 +4,16 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from datetime import date + from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective from myst_parser import __version__ # -- Project information ----------------------------------------------------- project = "MyST Parser" -copyright = "2020, Executable Book Project" +copyright = f"{date.today().year}, Executable Book Project" author = "Executable Book Project" version = __version__ @@ -29,8 +30,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", - "sphinxcontrib.bibtex", - "sphinx_panels", + "sphinx_design", "sphinxext.rediraffe", "sphinxcontrib.mermaid", "sphinxext.opengraph", @@ -55,11 +55,13 @@ 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", - "use_edit_page_button": True, "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" @@ -93,54 +95,25 @@ myst_heading_anchors = 2 myst_footnote_transition = True myst_dmath_double_inline = True -panels_add_bootstrap_css = False -bibtex_bibfiles = ["examples/references.bib"] + 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"] -def run_apidoc(app): - """generate apidoc - - See: https://github.com/rtfd/readthedocs.org/issues/1139 - """ - import os - import shutil - - import sphinx - from sphinx.ext import apidoc - - logger = sphinx.util.logging.getLogger(__name__) - logger.info("running apidoc") - # get correct paths - this_folder = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) - api_folder = os.path.join(this_folder, "_api") - module_path = os.path.normpath(os.path.join(this_folder, "../")) - ignore_paths = ["../setup.py", "../conftest.py", "../tests"] - ignore_paths = [ - os.path.normpath(os.path.join(this_folder, p)) for p in ignore_paths - ] - - if os.path.exists(api_folder): - shutil.rmtree(api_folder) - os.mkdir(api_folder) - - argv = ["-M", "--separate", "-o", api_folder, module_path] + ignore_paths - - apidoc.main(argv) - - # we don't use this - if os.path.exists(os.path.join(api_folder, "modules.rst")): - os.remove(os.path.join(api_folder, "modules.rst")) - - intersphinx_mapping = { "python": ("https://docs.python.org/3.7", None), "sphinx": ("https://www.sphinx-doc.org/en/master", None), @@ -170,6 +143,12 @@ def run_apidoc(app): ("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"), ] @@ -177,32 +156,13 @@ def run_apidoc(app): def setup(app: Sphinx): """Add functions to the Sphinx setup.""" + from myst_parser._docs import ( + DirectiveDoc, + DocutilsCliHelpDirective, + MystConfigDirective, + ) - class DocutilsCliHelpDirective(SphinxDirective): - """Directive to print the docutils CLI help.""" - - has_content = False - required_arguments = 0 - optional_arguments = 0 - final_argument_whitespace = False - option_spec = {} - - def run(self): - """Run the directive.""" - import io - - from docutils import nodes - from docutils.frontend import OptionParser - - from myst_parser.docutils_ import Parser as DocutilsParser - - stream = io.StringIO() - OptionParser( - components=(DocutilsParser,), - usage="myst-docutils-<writer> [options] [<source> [<destination>]]", - ).print_help(stream) - return [nodes.literal_block("", stream.getvalue())] - - # app.connect("builder-inited", run_apidoc) 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) diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..a87ce0bd --- /dev/null +++ b/docs/configuration.md @@ -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 diff --git a/docs/explain/index.md b/docs/develop/background.md similarity index 99% rename from docs/explain/index.md rename to docs/develop/background.md index 1ac48703..4be19e03 100644 --- a/docs/explain/index.md +++ b/docs/develop/background.md @@ -1,4 +1,4 @@ -# Background and explanation +# Background These sections discuss high-level questions about the MyST ecosystem, and explain a few decisions made in the project. diff --git a/docs/develop/index.md b/docs/develop/index.md index 9c7ba25f..f3a3f977 100644 --- a/docs/develop/index.md +++ b/docs/develop/index.md @@ -1,4 +1,4 @@ -# Contribute to MyST +# Contribute This section covers documentation relevant to developing and maintaining the MyST codebase, and some guidelines for how you can contribute. diff --git a/docs/docutils.md b/docs/docutils.md index 0e5c9411..b4d04800 100644 --- a/docs/docutils.md +++ b/docs/docutils.md @@ -1,6 +1,6 @@ (myst-docutils)= -# MyST with Docutils +# Single Page Builds ```{versionadded} 0.16.0 ``` diff --git a/docs/examples/heavy_tails.md b/docs/examples/heavy_tails.md deleted file mode 100644 index 50e2f811..00000000 --- a/docs/examples/heavy_tails.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -orphan: true ---- - -# DUMMY DOCUMENT FOR TESTING :doc: ROLE diff --git a/docs/examples/htop_again.png b/docs/examples/htop_again.png deleted file mode 100644 index 59168b560317569c423f71fe2d993de263d33013..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99091 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 C7gam} diff --git a/docs/examples/index.md b/docs/examples/index.md deleted file mode 100644 index f80832da..00000000 --- a/docs/examples/index.md +++ /dev/null @@ -1,32 +0,0 @@ -# Example pages - -MyST brings all of the features of reStructuredText into markdown. As an example, -The pages below have the same final product, but are written in either reStructuredText -or MyST markdown. You can browse the raw content of each page by clicking the -"download" button at the top of each page, or see the raw content below. - -```{toctree} ---- -caption: Compare rST and MyST versions -maxdepth: 1 ---- -wealth_dynamics_rst.rst -wealth_dynamics_md.md -``` - -## Raw content of each document above - -The following tabs show the raw content of each of the above documents, for quick -comparison. - -````{tabbed} MyST Markdown -Raw source for {doc}`wealth_dynamics_md` -```{literalinclude} wealth_dynamics_md.md -``` -```` - -````{tabbed} reStructuredText -Raw source for {doc}`wealth_dynamics_rst` -```{literalinclude} wealth_dynamics_rst.rst -``` -```` diff --git a/docs/examples/kesten_processes.md b/docs/examples/kesten_processes.md deleted file mode 100644 index 3589f61f..00000000 --- a/docs/examples/kesten_processes.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -orphan: true ---- -# DUMMY DOCUMENT FOR TESTING :doc: ROLE diff --git a/docs/examples/references.bib b/docs/examples/references.bib deleted file mode 100644 index 9cf5b215..00000000 --- a/docs/examples/references.bib +++ /dev/null @@ -1,12 +0,0 @@ -### -Single example from the QuantEcon Bib -### -@article{benhabib2018skewed, - title={Skewed wealth distributions: Theory and empirics}, - author={Benhabib, Jess and Bisin, Alberto}, - journal={Journal of Economic Literature}, - volume={56}, - number={4}, - pages={1261--91}, - year={2018} -} diff --git a/docs/examples/wealth_dynamics_md.md b/docs/examples/wealth_dynamics_md.md deleted file mode 100644 index 3af6672b..00000000 --- a/docs/examples/wealth_dynamics_md.md +++ /dev/null @@ -1,482 +0,0 @@ -# Wealth Distribution Dynamics in MyST - -> {sub-ref}`today` | {sub-ref}`wordcount-minutes` min read - -```{note} -You can {download}`Download the source file for this page <./wealth_dynamics_md.md>` -``` - -```{contents} -:depth: 2 -``` - -In addition to what's in Anaconda, this lecture will need the following -libraries: - -```{code-block} ipython ---- -class: hide-output ---- -!pip install --upgrade quantecon -``` - -## Overview - -This notebook gives an introduction to wealth distribution dynamics, -with a focus on - -- modeling and computing the wealth distribution via simulation, -- measures of inequality such as the Lorenz curve and Gini - coefficient, and -- how inequality is affected by the properties of wage income and - returns on assets. - -The wealth distribution in many countries exhibits a Pareto tail - -- See {doc}`this lecture <heavy_tails>` for a - definition. -- For a review of the empirical evidence, see, for example, - {cite}`md-benhabib2018skewed`. - -### A Note on Assumptions - -The evolution of wealth for any given household depends on their savings -behavior. - -We will use the following imports. - -```{code-block} python -import numpy as np -import matplotlib.pyplot as plt -%matplotlib inline - -import quantecon as qe -from numba import njit, jitclass, float64, prange -``` - -## Lorenz Curves and the Gini Coefficient - -Before we investigate wealth dynamics, we briefly review some measures -of inequality. - -### Lorenz Curves - -One popular graphical measure of inequality is the [Lorenz curve](https://en.wikipedia.org/wiki/Lorenz_curve). - -The package [QuantEcon.py](https://github.com/QuantEcon/QuantEcon.py), -already imported above, contains a function to compute Lorenz curves. - -To illustrate, suppose that - -```{code-block} python -n = 10_000 # size of sample -w = np.exp(np.random.randn(n)) # lognormal draws -``` - -is data representing the wealth of 10,000 households. - -We can compute and plot the Lorenz curve as follows: - -```{code-block} python -f_vals, l_vals = qe.lorenz_curve(w) - -fig, ax = plt.subplots() -ax.plot(f_vals, l_vals, label='Lorenz curve, lognormal sample') -ax.plot(f_vals, f_vals, label='Lorenz curve, equality') -ax.legend() -plt.show() -``` - -This curve can be understood as follows: if point $(x,y)$ lies on the -curve, it means that, collectively, the bottom $(100x)\%$ of the -population holds $(100y)\%$ of the wealth. - -```{code-block} python -a_vals = (1, 2, 5) # Pareto tail index -n = 10_000 # size of each sample -fig, ax = plt.subplots() -for a in a_vals: - u = np.random.uniform(size=n) - y = u**(-1/a) # distributed as Pareto with tail index a - f_vals, l_vals = qe.lorenz_curve(y) - ax.plot(f_vals, l_vals, label=f'$a = {a}$') -ax.plot(f_vals, f_vals, label='equality') -ax.legend() -plt.show() -``` - -You can see that, as the tail parameter of the Pareto distribution -increases, inequality decreases. - -This is to be expected, because a higher tail index implies less weight -in the tail of the Pareto distribution. - -### The Gini Coefficient - -The definition and interpretation of the Gini coefficient can be found -on the corresponding [Wikipedia page](https://en.wikipedia.org/wiki/Gini\_coefficient). - -A value of 0 indicates perfect equality (corresponding the case where -the Lorenz curve matches the 45 degree line) and a value of 1 indicates -complete inequality (all wealth held by the richest household). - -The [QuantEcon.py](https://github.com/QuantEcon/QuantEcon.py) library -contains a function to calculate the Gini coefficient. - -We can test it on the Weibull distribution with parameter $a$, where the -Gini coefficient is known to be - -$$G = 1 - 2^{-1/a}$$ - -Let's see if the Gini coefficient computed from a simulated sample -matches this at each fixed value of $a$. - -```{code-block} python -a_vals = range(1, 20) -ginis = [] -ginis_theoretical = [] -n = 100 - -fig, ax = plt.subplots() -for a in a_vals: - y = np.random.weibull(a, size=n) - ginis.append(qe.gini_coefficient(y)) - ginis_theoretical.append(1 - 2**(-1/a)) -ax.plot(a_vals, ginis, label='estimated gini coefficient') -ax.plot(a_vals, ginis_theoretical, label='theoretical gini coefficient') -ax.legend() -ax.set_xlabel("Weibull parameter $a$") -ax.set_ylabel("Gini coefficient") -plt.show() -``` - -The simulation shows that the fit is good. - -## A Model of Wealth Dynamics - -Having discussed inequality measures, let us now turn to wealth -dynamics. - -The model we will study is - -```{math} ---- -label: md:wealth_dynam_ah ---- -w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1} -``` - -where - -- $w_t$ is wealth at time $t$ for a given household, -- $r_t$ is the rate of return of financial assets, -- $y_t$ is current non-financial (e.g., labor) income and -- $s(w_t)$ is current wealth net of consumption - -Letting $\{z_t\}$ be a correlated state process of the form - -$$z_{t+1} = a z_t + b + \sigma_z \epsilon_{t+1}$$ - -we'll assume that - -$$R_t := 1 + r_t = c_r \exp(z_t) + \exp(\mu_r + \sigma_r \xi_t)$$ - -and - -$$y_t = c_y \exp(z_t) + \exp(\mu_y + \sigma_y \zeta_t)$$ - -Here $\{ (\epsilon_t, \xi_t, \zeta_t) \}$ is IID and standard normal in -$\mathbb R^3$. - -(md:sav_ah)= - -```{math} ---- -label: md:sav_ah ---- -s(w) = s_0 w \cdot \mathbb 1\{w \geq \hat w\} -``` - - -where $s_0$ is a positive constant. - -## Implementation - -Here's some type information to help Numba. - -```{code-block} python -wealth_dynamics_data = [ - ('w_hat', float64), # savings parameter - ('s_0', float64), # savings parameter - ('c_y', float64), # labor income parameter - ('μ_y', float64), # labor income paraemter - ('σ_y', float64), # labor income parameter - ('c_r', float64), # rate of return parameter - ('μ_r', float64), # rate of return parameter - ('σ_r', float64), # rate of return parameter - ('a', float64), # aggregate shock parameter - ('b', float64), # aggregate shock parameter - ('σ_z', float64), # aggregate shock parameter - ('z_mean', float64), # mean of z process - ('z_var', float64), # variance of z process - ('y_mean', float64), # mean of y process - ('R_mean', float64) # mean of R process -] -``` - -Here's a class that stores instance data and implements methods that -update the aggregate state and household wealth. - -```{code-block} python -@jitclass(wealth_dynamics_data) -class WealthDynamics: - - def __init__(self, - w_hat=1.0, - s_0=0.75, - c_y=1.0, - μ_y=1.0, - σ_y=0.2, - c_r=0.05, - μ_r=0.1, - σ_r=0.5, - a=0.5, - b=0.0, - σ_z=0.1): - - self.w_hat, self.s_0 = w_hat, s_0 - self.c_y, self.μ_y, self.σ_y = c_y, μ_y, σ_y - self.c_r, self.μ_r, self.σ_r = c_r, μ_r, σ_r - self.a, self.b, self.σ_z = a, b, σ_z - - # Record stationary moments - self.z_mean = b / (1 - a) - self.z_var = σ_z**2 / (1 - a**2) - exp_z_mean = np.exp(self.z_mean + self.z_var / 2) - self.R_mean = c_r * exp_z_mean + np.exp(μ_r + σ_r**2 / 2) - self.y_mean = c_y * exp_z_mean + np.exp(μ_y + σ_y**2 / 2) - - # Test a stability condition that ensures wealth does not diverge - # to infinity. - α = self.R_mean * self.s_0 - if α >= 1: - raise ValueError("Stability condition failed.") - - def parameters(self): - """ - Collect and return parameters. - """ - parameters = (self.w_hat, self.s_0, - self.c_y, self.μ_y, self.σ_y, - self.c_r, self.μ_r, self.σ_r, - self.a, self.b, self.σ_z) - return parameters - - def update_states(self, w, z): - """ - Update one period, given current wealth w and persistent - state z. - """ - - # Simplify names - params = self.parameters() - w_hat, s_0, c_y, μ_y, σ_y, c_r, μ_r, σ_r, a, b, σ_z = params - zp = a * z + b + σ_z * np.random.randn() - - # Update wealth - y = c_y * np.exp(zp) + np.exp(μ_y + σ_y * np.random.randn()) - wp = y - if w >= w_hat: - R = c_r * np.exp(zp) + np.exp(μ_r + σ_r * np.random.randn()) - wp += R * s_0 * w - return wp, zp -``` - -Here's function to simulate the time series of wealth for in individual -households. - -```{code-block} python -@njit -def wealth_time_series(wdy, w_0, n): - """ - Generate a single time series of length n for wealth given - initial value w_0. - - The initial persistent state z_0 for each household is drawn from - the stationary distribution of the AR(1) process. - - * wdy: an instance of WealthDynamics - * w_0: scalar - * n: int - - - """ - z = wdy.z_mean + np.sqrt(wdy.z_var) * np.random.randn() - w = np.empty(n) - w[0] = w_0 - for t in range(n-1): - w[t+1], z = wdy.update_states(w[t], z) - return w -``` - -Now here's function to simulate a cross section of households forward -in time. - -Note the use of parallelization to speed up computation. - -```{code-block} python -@njit(parallel=True) -def update_cross_section(wdy, w_distribution, shift_length=500): - """ - Shifts a cross-section of household forward in time - - * wdy: an instance of WealthDynamics - * w_distribution: array_like, represents current cross-section - - Takes a current distribution of wealth values as w_distribution - and updates each w_t in w_distribution to w_{t+j}, where - j = shift_length. - - Returns the new distribution. - - """ - new_distribution = np.empty_like(w_distribution) - - # Update each household - for i in prange(len(new_distribution)): - z = wdy.z_mean + np.sqrt(wdy.z_var) * np.random.randn() - w = w_distribution[i] - for t in range(shift_length-1): - w, z = wdy.update_states(w, z) - new_distribution[i] = w - return new_distribution -``` - -Parallelization is very effective in the function above because the time -path of each household can be calculated independently once the path for -the aggregate state is known. - -## Applications - -Let's try simulating the model at different parameter values and -investigate the implications for the wealth distribution. - -### Time Series - -Let's look at the wealth dynamics of an individual household. - -```{code-block} python -wdy = WealthDynamics() - -ts_length = 200 -w = wealth_time_series(wdy, wdy.y_mean, ts_length) - -fig, ax = plt.subplots() -ax.plot(w) -plt.show() -``` - -Notice the large spikes in wealth over time. - -Such spikes are similar to what we observed in time series when -{doc}`we studied Kesten processes<kesten_processes>`. - -### Inequality Measures - -Let's look at how inequality varies with returns on financial assets. - -The next function generates a cross section and then computes the Lorenz -curve and Gini coefficient. - -```{code-block} python -def generate_lorenz_and_gini(wdy, num_households=100_000, T=500): - """ - Generate the Lorenz curve data and gini coefficient corresponding to a - WealthDynamics mode by simulating num_households forward to time T. - """ - ψ_0 = np.ones(num_households) * wdy.y_mean - z_0 = wdy.z_mean - - ψ_star = update_cross_section(wdy, ψ_0, shift_length=T) - return qe.gini_coefficient(ψ_star), qe.lorenz_curve(ψ_star) -``` - -Now we investigate how the Lorenz curves associated with the wealth -distribution change as return to savings varies. - -The code below plots Lorenz curves for three different values of $\mu_r$. - -If you are running this yourself, note that it will take one or two -minutes to execute. - -This is unavoidable because we are executing a CPU intensive task. - -In fact the code, which is JIT compiled and parallelized, runs extremely -fast relative to the number of computations. - -```{code-block} python -fig, ax = plt.subplots() -μ_r_vals = (0.0, 0.025, 0.05) -gini_vals = [] - -for μ_r in μ_r_vals: - wdy = WealthDynamics(μ_r=μ_r) - gv, (f_vals, l_vals) = generate_lorenz_and_gini(wdy) - ax.plot(f_vals, l_vals, label=f'$\psi^*$ at $\mu_r = {μ_r:0.2}$') - gini_vals.append(gv) - -ax.plot(f_vals, f_vals, label='equality') -ax.legend(loc="upper left") -plt.show() -``` - -The Lorenz curve shifts downwards as returns on financial income rise, -indicating a rise in inequality. - -(htop_again)= - -```{image} htop_again.png ---- -scale: 80 ---- -``` - -Now let's check the Gini coefficient. - -```{code-block} python -fig, ax = plt.subplots() -ax.plot(μ_r_vals, gini_vals, label='gini coefficient') -ax.set_xlabel("$\mu_r$") -ax.legend() -plt.show() -``` - -Once again, we see that inequality increases as returns on financial -income rise. - -Let's finish this section by investigating what happens when we change -the volatility term $\sigma_r$ in financial returns. - -```{code-block} python -fig, ax = plt.subplots() -σ_r_vals = (0.35, 0.45, 0.52) -gini_vals = [] - -for σ_r in σ_r_vals: - wdy = WealthDynamics(σ_r=σ_r) - gv, (f_vals, l_vals) = generate_lorenz_and_gini(wdy) - ax.plot(f_vals, l_vals, label=f'$\psi^*$ at $\sigma_r = {σ_r:0.2}$') - gini_vals.append(gv) - -ax.plot(f_vals, f_vals, label='equality') -ax.legend(loc="upper left") -plt.show() -``` - -We see that greater volatility has the effect of increasing inequality -in this model. - -```{bibliography} references.bib -:labelprefix: md -:keyprefix: md- -``` diff --git a/docs/examples/wealth_dynamics_rst.rst b/docs/examples/wealth_dynamics_rst.rst deleted file mode 100644 index f9b64024..00000000 --- a/docs/examples/wealth_dynamics_rst.rst +++ /dev/null @@ -1,493 +0,0 @@ -.. highlight:: python3 - - -*********************************** -Wealth Distribution Dynamics in rST -*********************************** - - -.. note:: - - You can - :download:`Download the source file for this page <./wealth_dynamics_rst.rst>` - -.. contents:: :depth: 2 - -In addition to what's in Anaconda, this lecture will need the following libraries: - -.. code-block:: ipython - :class: hide-output - - !pip install --upgrade quantecon - - -Overview -======== - -This notebook gives an introduction to wealth distribution dynamics, with a -focus on - -* modeling and computing the wealth distribution via simulation, - -* measures of inequality such as the Lorenz curve and Gini coefficient, and - -* how inequality is affected by the properties of wage income and returns on assets. - -The wealth distribution in many countries exhibits a Pareto tail - -* See :doc:`this lecture <heavy_tails>` for a definition. - -* For a review of the empirical evidence, see, for example, :cite:`benhabib2018skewed`. - - - -A Note on Assumptions ---------------------- - -The evolution of wealth for any given household depends on their -savings behavior. - -We will use the following imports. - -.. code:: ipython3 - - import numpy as np - import matplotlib.pyplot as plt - %matplotlib inline - - import quantecon as qe - from numba import njit, jitclass, float64, prange - - -Lorenz Curves and the Gini Coefficient -====================================== - -Before we investigate wealth dynamics, we briefly review some measures of -inequality. - -Lorenz Curves -------------- - -One popular graphical measure of inequality is the `Lorenz curve -<https://en.wikipedia.org/wiki/Lorenz_curve>`__. - -The package `QuantEcon.py <https://github.com/QuantEcon/QuantEcon.py>`__, -already imported above, contains a function to compute Lorenz curves. - -To illustrate, suppose that - -.. code:: ipython3 - - n = 10_000 # size of sample - w = np.exp(np.random.randn(n)) # lognormal draws - -is data representing the wealth of 10,000 households. - -We can compute and plot the Lorenz curve as follows: - -.. code:: ipython3 - - f_vals, l_vals = qe.lorenz_curve(w) - - fig, ax = plt.subplots() - ax.plot(f_vals, l_vals, label='Lorenz curve, lognormal sample') - ax.plot(f_vals, f_vals, label='Lorenz curve, equality') - ax.legend() - plt.show() - -This curve can be understood as follows: if point :math:`(x,y)` lies on the curve, it means that, collectively, the bottom :math:`(100x)\%` of the population holds :math:`(100y)\%` of the wealth. - - -.. code:: ipython3 - - a_vals = (1, 2, 5) # Pareto tail index - n = 10_000 # size of each sample - fig, ax = plt.subplots() - for a in a_vals: - u = np.random.uniform(size=n) - y = u**(-1/a) # distributed as Pareto with tail index a - f_vals, l_vals = qe.lorenz_curve(y) - ax.plot(f_vals, l_vals, label=f'$a = {a}$') - ax.plot(f_vals, f_vals, label='equality') - ax.legend() - plt.show() - -You can see that, as the tail parameter of the Pareto distribution increases, inequality decreases. - -This is to be expected, because a higher tail index implies less weight in the tail of the Pareto distribution. - - - -The Gini Coefficient --------------------- - -The definition and interpretation of the Gini coefficient can be found on the -corresponding `Wikipedia page -<https://en.wikipedia.org/wiki/Gini_coefficient>`__. - -A value of 0 indicates perfect equality (corresponding the case where the -Lorenz curve matches the 45 degree line) and a value of 1 indicates complete -inequality (all wealth held by the richest household). - -The `QuantEcon.py <https://github.com/QuantEcon/QuantEcon.py>`__ library -contains a function to calculate the Gini coefficient. - -We can test it on the Weibull distribution with parameter :math:`a`, where the -Gini coefficient is known to be - -.. math:: G = 1 - 2^{-1/a} - -Let's see if the Gini coefficient computed from a simulated sample matches -this at each fixed value of :math:`a`. - - - -.. code:: ipython3 - - a_vals = range(1, 20) - ginis = [] - ginis_theoretical = [] - n = 100 - - fig, ax = plt.subplots() - for a in a_vals: - y = np.random.weibull(a, size=n) - ginis.append(qe.gini_coefficient(y)) - ginis_theoretical.append(1 - 2**(-1/a)) - ax.plot(a_vals, ginis, label='estimated gini coefficient') - ax.plot(a_vals, ginis_theoretical, label='theoretical gini coefficient') - ax.legend() - ax.set_xlabel("Weibull parameter $a$") - ax.set_ylabel("Gini coefficient") - plt.show() - -The simulation shows that the fit is good. - - - -A Model of Wealth Dynamics -========================== - -Having discussed inequality measures, let us now turn to wealth dynamics. - -The model we will study is - -.. math:: - :label: wealth_dynam_ah - - w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1} - -where - -- :math:`w_t` is wealth at time :math:`t` for a given household, -- :math:`r_t` is the rate of return of financial assets, -- :math:`y_t` is current non-financial (e.g., labor) income and -- :math:`s(w_t)` is current wealth net of consumption - -Letting :math:`\{z_t\}` be a correlated state process of the form - -.. math:: z_{t+1} = a z_t + b + \sigma_z \epsilon_{t+1} - -we’ll assume that - -.. math:: R_t := 1 + r_t = c_r \exp(z_t) + \exp(\mu_r + \sigma_r \xi_t) - -and - -.. math:: y_t = c_y \exp(z_t) + \exp(\mu_y + \sigma_y \zeta_t) - -Here :math:`\{ (\epsilon_t, \xi_t, \zeta_t) \}` is IID and standard -normal in :math:`\mathbb R^3`. - - -.. math:: - :label: sav_ah - - s(w) = s_0 w \cdot \mathbb 1\{w \geq \hat w\} - -where :math:`s_0` is a positive constant. - - -Implementation -============== - -Here's some type information to help Numba. - -.. code:: ipython3 - - wealth_dynamics_data = [ - ('w_hat', float64), # savings parameter - ('s_0', float64), # savings parameter - ('c_y', float64), # labor income parameter - ('μ_y', float64), # labor income paraemter - ('σ_y', float64), # labor income parameter - ('c_r', float64), # rate of return parameter - ('μ_r', float64), # rate of return parameter - ('σ_r', float64), # rate of return parameter - ('a', float64), # aggregate shock parameter - ('b', float64), # aggregate shock parameter - ('σ_z', float64), # aggregate shock parameter - ('z_mean', float64), # mean of z process - ('z_var', float64), # variance of z process - ('y_mean', float64), # mean of y process - ('R_mean', float64) # mean of R process - ] - -Here's a class that stores instance data and implements methods that update -the aggregate state and household wealth. - -.. code:: ipython3 - - @jitclass(wealth_dynamics_data) - class WealthDynamics: - - def __init__(self, - w_hat=1.0, - s_0=0.75, - c_y=1.0, - μ_y=1.0, - σ_y=0.2, - c_r=0.05, - μ_r=0.1, - σ_r=0.5, - a=0.5, - b=0.0, - σ_z=0.1): - - self.w_hat, self.s_0 = w_hat, s_0 - self.c_y, self.μ_y, self.σ_y = c_y, μ_y, σ_y - self.c_r, self.μ_r, self.σ_r = c_r, μ_r, σ_r - self.a, self.b, self.σ_z = a, b, σ_z - - # Record stationary moments - self.z_mean = b / (1 - a) - self.z_var = σ_z**2 / (1 - a**2) - exp_z_mean = np.exp(self.z_mean + self.z_var / 2) - self.R_mean = c_r * exp_z_mean + np.exp(μ_r + σ_r**2 / 2) - self.y_mean = c_y * exp_z_mean + np.exp(μ_y + σ_y**2 / 2) - - # Test a stability condition that ensures wealth does not diverge - # to infinity. - α = self.R_mean * self.s_0 - if α >= 1: - raise ValueError("Stability condition failed.") - - def parameters(self): - """ - Collect and return parameters. - """ - parameters = (self.w_hat, self.s_0, - self.c_y, self.μ_y, self.σ_y, - self.c_r, self.μ_r, self.σ_r, - self.a, self.b, self.σ_z) - return parameters - - def update_states(self, w, z): - """ - Update one period, given current wealth w and persistent - state z. - """ - - # Simplify names - params = self.parameters() - w_hat, s_0, c_y, μ_y, σ_y, c_r, μ_r, σ_r, a, b, σ_z = params - zp = a * z + b + σ_z * np.random.randn() - - # Update wealth - y = c_y * np.exp(zp) + np.exp(μ_y + σ_y * np.random.randn()) - wp = y - if w >= w_hat: - R = c_r * np.exp(zp) + np.exp(μ_r + σ_r * np.random.randn()) - wp += R * s_0 * w - return wp, zp - - -Here's function to simulate the time series of wealth for in individual households. - -.. code:: ipython3 - - @njit - def wealth_time_series(wdy, w_0, n): - """ - Generate a single time series of length n for wealth given - initial value w_0. - - The initial persistent state z_0 for each household is drawn from - the stationary distribution of the AR(1) process. - - * wdy: an instance of WealthDynamics - * w_0: scalar - * n: int - - - """ - z = wdy.z_mean + np.sqrt(wdy.z_var) * np.random.randn() - w = np.empty(n) - w[0] = w_0 - for t in range(n-1): - w[t+1], z = wdy.update_states(w[t], z) - return w - - -Now here's function to simulate a cross section of households forward in time. - -Note the use of parallelization to speed up computation. - -.. code:: ipython3 - - @njit(parallel=True) - def update_cross_section(wdy, w_distribution, shift_length=500): - """ - Shifts a cross-section of household forward in time - - * wdy: an instance of WealthDynamics - * w_distribution: array_like, represents current cross-section - - Takes a current distribution of wealth values as w_distribution - and updates each w_t in w_distribution to w_{t+j}, where - j = shift_length. - - Returns the new distribution. - - """ - new_distribution = np.empty_like(w_distribution) - - # Update each household - for i in prange(len(new_distribution)): - z = wdy.z_mean + np.sqrt(wdy.z_var) * np.random.randn() - w = w_distribution[i] - for t in range(shift_length-1): - w, z = wdy.update_states(w, z) - new_distribution[i] = w - return new_distribution - -Parallelization is very effective in the function above because the time path -of each household can be calculated independently once the path for the -aggregate state is known. - - - - -Applications -============ - -Let's try simulating the model at different parameter values and investigate -the implications for the wealth distribution. - - -Time Series ------------ - -Let's look at the wealth dynamics of an individual household. - -.. code:: ipython3 - - wdy = WealthDynamics() - - ts_length = 200 - w = wealth_time_series(wdy, wdy.y_mean, ts_length) - - fig, ax = plt.subplots() - ax.plot(w) - plt.show() - -Notice the large spikes in wealth over time. - -Such spikes are similar to what we observed in time series when :doc:`we studied Kesten processes <kesten_processes>`. - - - -Inequality Measures -------------------- - - -Let's look at how inequality varies with returns on financial assets. - -The next function generates a cross section and then computes the Lorenz -curve and Gini coefficient. - -.. code:: ipython3 - - def generate_lorenz_and_gini(wdy, num_households=100_000, T=500): - """ - Generate the Lorenz curve data and gini coefficient corresponding to a - WealthDynamics mode by simulating num_households forward to time T. - """ - ψ_0 = np.ones(num_households) * wdy.y_mean - z_0 = wdy.z_mean - - ψ_star = update_cross_section(wdy, ψ_0, shift_length=T) - return qe.gini_coefficient(ψ_star), qe.lorenz_curve(ψ_star) - -Now we investigate how the Lorenz curves associated with the wealth distribution change as return to savings varies. - -The code below plots Lorenz curves for three different values of :math:`\mu_r`. - -If you are running this yourself, note that it will take one or two minutes to execute. - -This is unavoidable because we are executing a CPU intensive task. - -In fact the code, which is JIT compiled and parallelized, runs extremely fast relative to the number of computations. - -.. code:: ipython3 - - fig, ax = plt.subplots() - μ_r_vals = (0.0, 0.025, 0.05) - gini_vals = [] - - for μ_r in μ_r_vals: - wdy = WealthDynamics(μ_r=μ_r) - gv, (f_vals, l_vals) = generate_lorenz_and_gini(wdy) - ax.plot(f_vals, l_vals, label=f'$\psi^*$ at $\mu_r = {μ_r:0.2}$') - gini_vals.append(gv) - - ax.plot(f_vals, f_vals, label='equality') - ax.legend(loc="upper left") - plt.show() - -The Lorenz curve shifts downwards as returns on financial income rise, indicating a rise in inequality. - - -.. _htop_again: - -.. figure:: htop_again.png - :scale: 80 - - -Now let's check the Gini coefficient. - -.. code:: ipython3 - - fig, ax = plt.subplots() - ax.plot(μ_r_vals, gini_vals, label='gini coefficient') - ax.set_xlabel("$\mu_r$") - ax.legend() - plt.show() - -Once again, we see that inequality increases as returns on financial income -rise. - -Let's finish this section by investigating what happens when we change the -volatility term :math:`\sigma_r` in financial returns. - - -.. code:: ipython3 - - fig, ax = plt.subplots() - σ_r_vals = (0.35, 0.45, 0.52) - gini_vals = [] - - for σ_r in σ_r_vals: - wdy = WealthDynamics(σ_r=σ_r) - gv, (f_vals, l_vals) = generate_lorenz_and_gini(wdy) - ax.plot(f_vals, l_vals, label=f'$\psi^*$ at $\sigma_r = {σ_r:0.2}$') - gini_vals.append(gv) - - ax.plot(f_vals, f_vals, label='equality') - ax.legend(loc="upper left") - plt.show() - - -We see that greater volatility has the effect of increasing inequality in this model. - -.. bibliography:: references.bib diff --git a/docs/sphinx/use.md b/docs/faq/index.md similarity index 78% rename from docs/sphinx/use.md rename to docs/faq/index.md index 1678d522..e4d45815 100644 --- a/docs/sphinx/use.md +++ b/docs/faq/index.md @@ -1,13 +1,13 @@ -# Sphinx extension usage guide +(myst-sphinx)= -These sections describe some common scenarios and use-cases for writing MyST with Sphinx. +# FAQ -:::{seealso} -For an introduction to using MyST with Sphinx, see [](intro.md). -::: +## 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 +### 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: @@ -33,7 +33,7 @@ To include rST, we must first "wrap" the directive in the [eval-rst directive](s ``` (howto/include-md)= -## Include Markdown files into an rST file +### 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: @@ -46,12 +46,12 @@ To include a MyST file within a ReStructuredText file, we can use the `parser` o The `parser` option requires `docutils>=0.17` ``` -## Use MyST in Jupyter Notebooks +### 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) +### 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.: @@ -100,7 +100,7 @@ If you encounter any issues with this feature, please don't hesitate to report i ::: (howto/autodoc)= -## Use `sphinx.ext.autodoc` in Markdown files +### 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. @@ -131,7 +131,7 @@ We hope to support Markdown in the future, see [GitHub issue #228](https://githu ``` (howto/autosectionlabel)= -## Automatically create targets for section headers +### Automatically create targets for section headers :::{important} @@ -173,7 +173,7 @@ like so: ``` (howto/warnings)= -## Suppress 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. @@ -195,7 +195,7 @@ suppress_warnings = ["myst.header"] ``` -## Sphinx-specific page front matter +### 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)). @@ -225,6 +225,43 @@ orphan: true This is an orphan document, not specified in any toctrees. ``` -## Migrate pre-existing rST into MyST +### 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. diff --git a/docs/sphinx/snippets/include-md.md b/docs/faq/snippets/include-md.md similarity index 100% rename from docs/sphinx/snippets/include-md.md rename to docs/faq/snippets/include-md.md diff --git a/docs/sphinx/snippets/include-rst.rst b/docs/faq/snippets/include-rst.rst similarity index 100% rename from docs/sphinx/snippets/include-rst.rst rename to docs/faq/snippets/include-rst.rst diff --git a/docs/index.md b/docs/index.md index 93d0fa67..a87e43f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,123 +1,145 @@ -# MyST - Markedly Structured Text <img src="_static/logo-square.svg" width=40 /> +--- +sd_hide_title: true +--- -[![PyPI][pypi-badge]][pypi-link] -[![Conda][conda-badge]][conda-link] +# Overview -**MyST is a rich and extensible flavor of Markdown meant for technical documentation and publishing**. +::::{grid} +:reverse: +:gutter: 3 4 4 4 +:margin: 1 2 1 2 -MyST is a flavor of markdown that is designed for simplicity, flexibility, and extensibility. Here are a few major features +:::{grid-item} +:columns: 12 4 4 4 -:::{panels} -:container: +full-width text-center -:column: col-lg-4 px-2 py-2 -:card: +```{image} _static/logo-square.svg +:width: 200px +:class: sd-m-auto +``` -**[CommonMark compliant](commonmark-block-tokens)** ✔ -^^^ -MyST is a superset of [CommonMark Markdown][commonmark]. Any CommonMark document is also MyST-compliant. ---- +::: -**[Extra syntax for authoring](extended-block-tokens)** ✍ -^^^ -MyST extends CommonMark with [syntax meant for scholarly writing and technical documentation](extended-block-tokens). +:::{grid-item} +:columns: 12 8 8 8 +:child-align: justify +:class: sd-fs-5 ---- -**[Extendable syntax](syntax/directives)** 🚀 -^^^ -MyST provides [roles](syntax/roles) and [directives](syntax/directives), allowing you to extend MyST's functionality. +```{rubric} MyST - Markedly Structured Text - Parser +``` ---- -**[Compatible with Sphinx](sphinx/index.md)** 📄 -^^^ -MyST is inspired by Sphinx, and comes with [its own Sphinx parser](sphinx/index.md). -[Write your Sphinx docs in Markdown](sphinx:usage/quickstart), or convert existing [RST to Markdown][rst-to-myst] -from the CLI or [using an interactive web interface][mystyc]! +A Sphinx and Docutils extension to parse MyST, +a rich and extensible flavour of Markdown for authoring technical and scientific documentation. ---- -**[Hackable with Python](api/index.md)** 🐍 -^^^ -This MyST parser is built on top of the [`markdown-it-py` package][markdown-it-py], an pluggable Python parser for Markdown. +```{button-ref} intro +:ref-type: doc +:color: primary +:class: sd-rounded-pill + +Get Started +``` + +::: + +:::: --- -**[Hackable with Javascript][markdown-it-myst]** 🌍 -^^^ -The [Javascript parser][markdown-it-myst] builds on [markdown-it][markdown-it], and allows you to parse MyST in websites. + +::::{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) ::: -## Find the right documentation resources +:::{grid-item-card} {octicon}`plug;1.5em;sd-mr-1` Sphinx compatible +:link: roles-directives +:link-type: ref -This documentation is organized into a few major sections. **Tutorials** are step-by-step introductory guides to MyST Markdown. **Topic Guides** cover specific areas in more depth, and are organized as discrete "how-to" sections. **Reference** sections describe the API/syntax/etc of the MyST Parser in detail. +Use the MyST role and directive syntax to harness the full capability of Sphinx, such as admonitions and figures, and all existing Sphinx extensions. -In addition, here are a few pointers to help you get started. ++++ +[Learn more »](roles-directives) +::: -:::{panels} -:container: full-width -:column: col-lg-4 p-2 ---- -:header: bg-myst-one -**Get started with MyST** -^^^ -**[](sphinx/intro.md)**: a step-by-step tutorial. +:::{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) +::: -**[](syntax/syntax.md)**: discusses major MyST syntax components. +:::: -**[The Sphinx guide](sphinx/index.md)**: how to use MyST with your Sphinx documentation. --- -:header: bg-myst-two -**Learn more about MyST** -^^^ -**[](syntax/optional.md)**: additional syntax you can enable for extra features. +```{rubric} Additional resources +``` -**[The Python API guide](api/index.md)**: parsing and rendering MyST with Python. +[MyST-Markdown VS Code extension](https://marketplace.visualstudio.com/items?itemName=ExecutableBookProject.myst-highlight) +: For MyST extended syntax highlighting and authoring tools. -**[](explain/index.md)**: background understanding and discussions of MyST markdown. ---- -:header: bg-myst-three +[Convert existing ReStructuredText files to Markdown][rst-to-myst] +: Use the [rst-to-myst] CLI or [the MySTyc interactive web interface](https://mystyc.herokuapp.com) -**Get inspired** -^^^ -**[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. +[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. -**[The Jupyter Book gallery](https://gallery.jupyterbook.org)**: examples of documents built with MyST. -::: +[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. -```{toctree} -:hidden: -sphinx/intro.md +[The Jupyter Book gallery](https://gallery.jupyterbook.org) +: 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} -:caption: MyST Syntax :hidden: -syntax/syntax -syntax/optional -syntax/reference +intro.md ``` ```{toctree} :hidden: -:caption: Topic Guides -explain/index.md -sphinx/index.md +:caption: Guides + +syntax/syntax +syntax/optional +syntax/roles-and-directives.md +configuration.md docutils.md -api/index.md +faq/index.md develop/index.md ``` ```{toctree} :hidden: -:caption: About the project -examples/index.md +:caption: Reference + develop/_changelog.md -GitHub repo <https://github.com/executablebooks/myst-parser> +syntax/reference +develop/background.md +api/reference.rst ``` -## Acknowledgements - -The MyST markdown language and MyST parser are both supported by the open community, -[The Executable Book Project](https://executablebooks.org). - [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 @@ -133,7 +155,6 @@ The MyST markdown language and MyST parser are both supported by the open commun [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-myst]: https://github.com/executablebooks/markdown-it-myst [markdown-it]: https://markdown-it.github.io/ [rst-to-myst]: https://rst-to-myst.readthedocs.io -[mystyc]: https://mystyc.herokuapp.com +[mystjs]: https://github.com/executablebooks/mystjs diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 00000000..b0b8d429 --- /dev/null +++ b/docs/intro.md @@ -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 + + + +
+

My nifty title

+ +

Some text!

+
+ + +``` + +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 +... +
+

Here's my title

+

Here's my admonition content.1

+
+... +``` + +:::{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 +... + +

A header

+ +

My reference

+... +``` + +:::{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 `sphinxcontib.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 `. +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
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
prevail... + John-->Alice: Great! + John->Bob: How about you? + Bob-->John: Jolly good! +``` diff --git a/docs/sphinx/faq.md b/docs/sphinx/faq.md deleted file mode 100644 index a799b168..00000000 --- a/docs/sphinx/faq.md +++ /dev/null @@ -1,16 +0,0 @@ -# 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 ` ``. While this looks like reStructuredText, the role may -be explicitly expecting the `My title ` structure, and so MyST will behave the same way. diff --git a/docs/sphinx/index.md b/docs/sphinx/index.md deleted file mode 100644 index afe7694c..00000000 --- a/docs/sphinx/index.md +++ /dev/null @@ -1,17 +0,0 @@ -(myst-sphinx)= - -# MyST with Sphinx - -The MyST Parser comes bundled with a Sphinx extension that allows you to write Sphinx documentation entirely in MyST (or, in a combination of rST and MyST). -The following sections cover some major functionality of the Sphinx extension. - -:::{seealso} -For an introduction to MyST with Sphinx, see [](intro.md). -::: - -```{toctree} -use.md -roles-and-directives.md -reference.md -faq.md -``` diff --git a/docs/sphinx/intro.md b/docs/sphinx/intro.md deleted file mode 100644 index 7c7f46b3..00000000 --- a/docs/sphinx/intro.md +++ /dev/null @@ -1,423 +0,0 @@ -(intro/get-started)= -# Get started with MyST in Sphinx - -This page describes how to get started with the MyST parser, with a focus on enabling it in the Sphinx documentation engine. - -## Install the MyST Parser - -[![PyPI][pypi-badge]][pypi-link] -[![Conda][conda-badge]][conda-link] - -Installing the MyST parser provides access to two tools: - -* A Python library that can parse MyST markdown, and render it to a number of output formats (in particular, `docutils` format for use with Sphinx). -* A Sphinx extension that that utilizes the above tool in order to parse MyST Markdown in your documentation. - -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 -``` - -[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 - -(parse-with-sphinx)= -## Enable MyST in Sphinx - -Sphinx is a documentation generator for building a website or book from multiple source documents and assets. To get started with Sphinx, see their [Quickstart Guide](https://www.sphinx-doc.org/en/master/usage/quickstart.html). This guide assumes that you've already got a pre-existing Sphinx site that builds properly. - -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. - -:::{admonition} You can use both MyST and reStructuredText -:class: tip - -Activating the MyST parser will simply *enable* parsing markdown files with MyST, and the rST parser that ships with Sphinx will still work the same way. -Files ending with `.md` will be parsed as MyST, and files ending in `.rst` will be parsed as reStructuredText. -::: - -(intro/writing)= -## Write your first markdown document - -Now that you've enabled the `myst-parser` in Sphinx, you can write MyST markdown in a file that ends with `.md` extension for your pages. - -:::{note} -MyST markdown is a mixture of two flavors of markdown: - -It supports all the syntax of **[CommonMark Markdown](https://commonmark.org/)** at its -base. This is a community standard flavor of markdown used across many projects. - -In addition, it includes **[several extensions](../syntax/syntax.md) to CommonMark**. -These add extra syntax features for technical writing, such as the roles and directives used by 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**! -``` - -In the "main document" of your Sphinx project (the landing page of your Sphinx documentation), include `myfile.md` in a `toctree` directive so that it is included in your documentation: - -```rst -.. toctree:: - - myfile.md -``` - -Now build your site: - -```bash -make html -``` - -and navigate to your landing page. -You should see a link to the page generated from `myfile.md`. -Clicking that link should take you to your rendered Markdown! - -## Extend markdown with a directive - -The most important functionality available with MyST markdown is writing **directives**. -Directives are kind-of like functions that are designed for writing content. -Sphinx and reStructuredText use directives extensively. -Here's how a directive looks in MyST markdown: - -````{margin} Alternative options syntax -If you've got a lot of options for your directive, or have a value that is really -long (e.g., that spans multiple lines), then you can also wrap your options in -`---` lines and write them as YAML. For example: - -```yaml ---- -key1: val1 -key2: | - val line 1 - val line 2 ---- -``` -```` - -```` -```{directivename} -:optionname: - - -``` -```` - -For those who are familiar with reStructuredText, you can find [a mapping from MyST directive syntax to rST syntax here](syntax/directives). - - -As seen above, there are four main parts to consider when writing directives. - -* **the directive name** is kind of like the function name. Different names trigger - different functionality. They are wrapped in `{}` brackets. -* **directive arguments** come just after the directive name. They can be used - to trigger behavior in the directive. -* **directive options** come just after the first line of the directive. They also - control behavior of the directive. -* **directive content** is markdown that you put inside the directive. The directive - often displays the content in a special way. - -For example, add an **`admonition`** directive to your markdown page, like so: - - -````md -# My nifty title - -Some **text**! - -```{admonition} Here's my title -:class: warning - -Here's my admonition content -``` -```` - -Re-build your Sphinx site and you should see the new admonition box show up. - -As you can see, we've used each of the four pieces described above to configure this -directive. Here's how the directive looks when rendered: - -```{admonition} Here's my title -:class: warning - -Here's my admonition content -``` - -:::{seealso} -For more information about using directives with MyST, see {ref}`syntax/directives`. -::: - -(sphinx/intro:reference)= -## Reference a section label with a role - -Roles are another core Sphinx tool. They behave similarly to directives, but are given -in-line with text instead of in a separate block. They have the following form: - -```md -{rolename}`role content` -``` - -Roles are a bit more simple than directives, though some roles allow for more complex syntax inside their content area. -For example, the `ref` role is used to make references to other sections of your documentation, and allows you to specify the displayed text as well as the reference itself within the role: - -```md -{ref}`My displayed text ` -``` - -For example, let's add a **section reference** to your markdown file. -To do this, we'll first need to add a **label** to a section of your page. -To do so, use the following structure: - -```md -(label-name)= -## Some header -``` - -Add this to your markdown file from above, like so: - -````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. -```` - -Because your new section has a label (`section-two`), you can reference it with the `ref` role. -Add it to your markdown file like so: - - -```md -(label-name)= -## Some header -``` - -Add this to your markdown file from above, like so: - -````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. - -And here's {ref}`a reference to this section `. -I can also reference the section {ref}`section-two` without specifying my title. -```` - -Re-build your documentation and you should see the references automatically inserted. -Here's an example of how the `ref` roles look in the final output: - -Here's a reference to {ref}`sphinx/intro:reference`. - -:::{seealso} -For more information about roles, see {ref}`syntax/roles`. -::: - -## Add a comment using extra MyST syntax - -There are many other kinds of syntax in MyST to make writing more productive and enjoyable. -Let's play around with a couple of options. - -First, try writing a **comment**. -This can be done by adding a line starting with `%` to your markdown file. -For example, try adding a comment to your markdown file, like so: - -````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 `. -I can also reference the section {ref}`section-two` without specifying my title. -```` - -Re-build your documentation - the comment should _not_ be present in the output. - -## Extending MyST via configuration - -Thus far we have covered the basic MyST syntax with Sphinx. -However, there are a few ways that you can _extend_ this base syntax and get new functionality. -The first is to enable some "out of the box" extensions with the MyST parser. -These add new syntax that aren't part of "core MyST" but that are useful nonetheless (and may become part of core MyST one day). - -Let's extend the base MyST syntax to enable **fences for directives**. -This allows you to define a directive with `:::` in addition to ` ``` `. -This is useful for directives that have markdown in their content. -By using `:::`, a non-MyST markdown renderer will still be able to render what is inside (instead of displaying it as a code block). - -To activate extensions, add a list to your `conf.py` file that contains the extensions you'd like to activate. -For example, to activate the "colon code fences" extension, add the following to your `conf.py` file: - -```python -myst_enable_extensions = [ - "colon_fence", -] -``` - -You may now use `:::` to define directives. -For example, modify your markdown file like so: - -````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 `. -I can also reference the section {ref}`section-two` without specifying my title. - -:::{note} -And here's a note with a colon fence! -::: -```` - -It should render as a "note block" in your output when you build your site. - -## Install a new Sphinx extension and use its functionality - -The other way to extend MyST in Sphinx is to install Sphinx extensions that define new directives. -Directives are kind of like "functions" in Sphinx, and installing a new package can add new directives to use in your content. - -For example, let's install the `sphinxcontib.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 `. -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
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
prevail... - John-->Alice: Great! - John->Bob: How about you? - Bob-->John: Jolly good! -``` - -## Next steps - Learn more about MyST Syntax - -In this tutorial we've covered some of the basics of MyST Markdown, how to enable and use it with Sphinx, and how to extend it for new use-cases. -There is much more functionality in MyST (and in the Sphinx ecosystem) that we haven't covered here. -For more information, see the [documentation on MyST Syntax](../syntax/syntax.md) and the [documentation about using MyST with Sphinx](../sphinx/index.md). diff --git a/docs/sphinx/reference.md b/docs/sphinx/reference.md deleted file mode 100644 index c77f559d..00000000 --- a/docs/sphinx/reference.md +++ /dev/null @@ -1,129 +0,0 @@ -(sphinx/config-options)= -# Sphinx configuration options - -You can control the behaviour of the MyST parser in Sphinx by modifying your `conf.py` file. -To do so, use the keywords beginning `myst_`. - -`````{list-table} -:header-rows: 1 - -* - Option - - Default - - Description -* - `myst_commonmark_only` - - `False` - - If `True` convert text as strict [CommonMark](https://spec.commonmark.org/) (all options below are then ignored). Note that strict CommonMark is unable to parse any directives, including the `toctree` directive, thus limiting MyST parser to single-page documentations. Use in conjunction with [sphinx-external-toc](https://github.com/executablebooks/sphinx-external-toc) Sphinx extension to counter this limitation. -* - `myst_gfm_only` - - `False` - - If `True` convert text as strict [GitHub-flavored Markdown](https://github.github.com/gfm/) (all options below are then ignored). -* - `myst_disable_syntax` - - () - - List of markdown syntax elements to disable, see the [markdown-it parser guide](markdown_it:using). -* - `myst_enable_extensions` - - `["dollarmath"]` - - Enable Markdown extensions, [see here](../syntax/optional.md) for details. -* - `myst_all_links_external` - - `False` - - If `True`, all Markdown links `[text](link)` are treated as external. -* - `myst_url_schemes` - - `None` - - [URI schemes](https://en.wikipedia.org/wiki/List_of_URI_schemes) that will be recognised as external URLs in `[](scheme:loc)` syntax, or set `None` to recognise all. - Other links will be resolved as internal cross-references. -* - `myst_ref_domains` - - `None` - - If a list, then only these [sphinx domains](sphinx:domain) will be searched for when resolving Markdown links like `[text](reference)`. -* - `myst_linkify_fuzzy_links` - - `True` - - If `False`, only links that contain a scheme (such as `http`) will be recognised as external links. -* - `myst_title_to_header` - - `False` - - If `True`, the `title` key of a document front-matter is converted to a header at the top of the document. -* - `myst_heading_anchors` - - `None` - - Enable auto-generated heading anchors, up to a maximum level, [see here](syntax/header-anchors) for details. -* - `myst_heading_slug_func` - - `None` - - Use the specified function to auto-generate heading anchors, [see here](syntax/header-anchors) for details. -* - `myst_number_code_blocks` - - `()` - - Add line numbers to code blocks with these languages, [see here](syntax/code-blocks) for details. -* - `myst_substitutions` - - `{}` - - A mapping of keys to substitutions, used globally for all MyST documents when the "substitution" extension is enabled. -* - `myst_html_meta` - - `{}` - - A mapping of keys to HTML metadata, used globally for all MyST documents. See [](syntax/html_meta). -* - `myst_footnote_transition` - - `True` - - Place a transition before any footnotes. -* - `myst_words_per_minute` - - `200` - - Reading speed used to calculate `` {sub-ref}`wordcount-minutes` `` -````` - -List of extensions: - -- "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 -- "html_admonition": Convert `
` elements to sphinx admonition nodes, see the [HTML admonition syntax](syntax/html-admonition) for details -- "fieldlist": Enable field lists, [see here](syntax/fieldlists) for details -- "html_image": Convert HTML `` elements to sphinx image nodes, see the [image syntax](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 -- "substitution": substitute keys, see the [substitutions syntax](syntax/substitutions) for details -- "tasklist": add check-boxes to the start of list items, see the [tasklist syntax](syntax/tasklists) for details - -Math specific, when `"dollarmath"` activated, see the [Math syntax](syntax/math) for more details: - -`````{list-table} -:header-rows: 1 - -* - Option - - Default - - Description -* - `myst_dmath_double_inline` - - `False` - - Allow display math (i.e. `$$`) within an inline context -* - `myst_dmath_allow_labels` - - `True` - - Parse `$$...$$ (label)` syntax -* - `myst_dmath_allow_space` - - `True` - - If False then inline math will only be parsed if there are no initial/final spaces, - e.g. `$a$` but not `$ a$` or `$a $` -* - `myst_dmath_allow_digits` - - `True` - - If False then inline math will only be parsed if there are no initial/final digits, - e.g. `$a$` but not `1$a$` or `$a$2` (this is useful for using `$` as currency) -* - `myst_amsmath_enable` - - `False` - - Enable direct parsing of [amsmath LaTeX environments](https://ctan.org/pkg/amsmath) -* - `myst_update_mathjax` - - `True` - - If using [sphinx.ext.mathjax](https://www.sphinx-doc.org/en/master/usage/extensions/math.html#module-sphinx.ext.mathjax) (the default) then `mathjax_config` will be updated, - to ignore `$` delimiters and LaTeX environments, which should instead be handled by - `myst_dmath_enable` and `myst_amsmath_enable` respectively. -````` - -## 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). diff --git a/docs/sphinx/roles-and-directives.md b/docs/sphinx/roles-and-directives.md deleted file mode 100644 index 63c86a31..00000000 --- a/docs/sphinx/roles-and-directives.md +++ /dev/null @@ -1,23 +0,0 @@ -# Special roles and directives - -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)). diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md index 9854d09a..4045eb2e 100644 --- a/docs/syntax/optional.md +++ b/docs/syntax/optional.md @@ -1,22 +1,23 @@ --- -substitutions: - key1: I'm a **substitution** - key2: | - ```{note} - {{ key1 }} - ``` - key3a: fishy - key3: | - ```{image} img/fun-fish.png - :alt: fishy - :width: 200px - ``` - key4: example +myst: + substitutions: + key1: I'm a **substitution** + key2: | + ```{note} + {{ key1 }} + ``` + key3a: fishy + key3: | + ```{image} img/fun-fish.png + :alt: fishy + :width: 200px + ``` + key4: example --- -(syntax/optional)= +(syntax/extensions)= -# Optional MyST Syntaxes +# Syntax Extensions MyST-Parser is highly configurable, utilising the inherent "plugability" of the [markdown-it-py](markdown_it:index) parser. The following syntaxes are optional (disabled by default) and can be enabled *via* the sphinx `conf.py` (see also [](sphinx/config-options)). @@ -74,7 +75,7 @@ text | converted ``--`` | -- ``---`` | --- -(syntax/strikethough)= +(syntax/strikethrough)= ## Strikethrough @@ -283,18 +284,19 @@ or at the top of the file, in the front-matter section (see [this section](synta ````yaml --- -substitutions: - key1: "I'm a **substitution**" - key2: | - ```{note} - {{ key1 }} - ``` - key3: | - ```{image} img/fun-fish.png - :alt: fishy - :width: 200px - ``` - key4: example +myst: + substitutions: + key1: "I'm a **substitution**" + key2: | + ```{note} + {{ key1 }} + ``` + key3: | + ```{image} img/fun-fish.png + :alt: fishy + :width: 200px + ``` + key4: example --- ```` @@ -304,7 +306,8 @@ Keys in the front-matter will override ones in the `conf.py`. You can use these substitutions inline or as blocks, and you can even nest substitutions in other substitutions (but circular references are prohibited): -:::{tabbed} Markdown Input +::::{tab-set} +:::{tab-item} Markdown Input ```md Inline: {{ key1 }} @@ -321,7 +324,7 @@ Block level: ::: -:::{tabbed} Rendered Output +:::{tab-item} Rendered Output Inline: {{ key1 }} Block level: @@ -333,6 +336,7 @@ Block level: | {{key2}} | {{key3}} | ::: +:::: :::{important} @@ -407,7 +411,8 @@ you can also use `:::` delimiters to denote code fences, instead of ```` ``` ``` Using colons instead of back-ticks has the benefit of allowing the content to be rendered correctly, when you are working in any standard Markdown editor. It is ideal for admonition type directives (as documented in [Directives](syntax/directives)) or tables with titles, for example: -````{tabbed} Markdown Input +::::::{tab-set} +:::::{tab-item} Markdown Input ```md :::{note} This text is **standard** _Markdown_ @@ -423,9 +428,9 @@ abc | mnp | xyz ::: ``` -```` +::::: -````{tabbed} Rendered Output +:::::{tab-item} Rendered Output :::{note} This text is **standard** _Markdown_ @@ -440,7 +445,8 @@ abc | mnp | xyz 123 | 456 | 789 ::: -```` +::::: +:::::: Similar to normal directives, these directives can also be nested: @@ -503,7 +509,7 @@ You can then insert markdown links directly to anchors that are generated from y For example `[](#auto-generated-header-anchors)`: [](#auto-generated-header-anchors). The paths to other files should be relative to the current file, for example -`[**link text**](./syntax.md#the-myst-syntax-guide)`: [**link text**](./syntax.md#the-myst-syntax-guide). +`[**link text**](./syntax.md#core-syntax)`: [**link text**](./syntax.md#core-syntax). ### Anchor slug structure diff --git a/docs/syntax/reference.md b/docs/syntax/reference.md index 91ff7b8d..1e53b43e 100644 --- a/docs/syntax/reference.md +++ b/docs/syntax/reference.md @@ -1,4 +1,5 @@ -# MyST Syntax Reference +(syntax-tokens)= +# Syntax tokens This page serves as a reference for the syntax that makes of MyST Markdown. @@ -13,8 +14,6 @@ Block tokens span multiple lines of content. They are broken down into two secti - {ref}`extended-block-tokens` contains *extra* tokens that are not in CommonMark. - {ref}`commonmark-block-tokens` contains CommonMark tokens that also work, for reference. -In addition to these summaries of block-level syntax, see {ref}`extra-markdown-syntax`. - :::{note} Because MyST markdown was inspired by functionality that exists in reStructuredText, we have shown equivalent rST syntax for many MyST markdown features below. @@ -164,8 +163,6 @@ 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. -In addition to these summaries of inline syntax, see {ref}`extra-markdown-syntax`. - (extended-span-tokens)= ### Extended inline tokens diff --git a/docs/syntax/roles-and-directives.md b/docs/syntax/roles-and-directives.md new file mode 100644 index 00000000..4b3d80a4 --- /dev/null +++ b/docs/syntax/roles-and-directives.md @@ -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 `, 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)). diff --git a/docs/syntax/syntax.md b/docs/syntax/syntax.md index 38436191..877afadc 100644 --- a/docs/syntax/syntax.md +++ b/docs/syntax/syntax.md @@ -1,398 +1,59 @@ -(example_syntax)= +(syntax/core)= -# The MyST Syntax Guide +# Core Syntax -> {sub-ref}`today` | {sub-ref}`wordcount-minutes` min read +## Introduction -As a base, MyST adheres to the [CommonMark specification](https://spec.commonmark.org/). -For this, it uses the [markdown-it-py](https://github.com/executablebooks/markdown-it-py) parser, -which is a well-structured markdown parser for Python that is CommonMark-compliant -and also extensible. +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. -MyST adds several new syntax options to CommonMark in order to be used -with Sphinx, the documentation generation engine used extensively in the Python -ecosystem. - -Below is a summary of the syntax 'tokens' parsed, -and further details of a few major extensions from the CommonMark flavor of markdown. +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} -- For an introduction to writing Directives and Roles with MyST markdown, see {ref}`intro/writing`. -- Check out the [MyST-Markdown VS Code extension](https://marketplace.visualstudio.com/items?itemName=ExecutableBookProject.myst-highlight), for MyST extended syntax highlighting. -::: - -MyST builds on the tokens defined by markdown-it, to extend the syntax -described in the [CommonMark Spec](https://spec.commonmark.org/0.29/), which the parser is tested against. - -% TODO link to markdown-it documentation - -(syntax/directives)= - -## Directives - a block-level extension point - -Directives syntax is defined with triple-backticks and curly-brackets. It -is effectively a code block with curly brackets around the language, and -a directive name in place of a language name. It is similar to how RMarkdown -defines "runnable cells". 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_ +The [syntax token reference tables](syntax-tokens) ::: -(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: +(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 | `` +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 ```` ``` ```` -```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 ---- -``` +(syntax/frontmatter)= -Euler's identity, equation {math:numref}`euler`, was elected one of the -most beautiful mathematical formulas. -```` +## Front Matter -Becomes: +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: -```{math} e^{i\pi} + 1 = 0 +```yaml --- -label: euler +key1: value +key2: [value1, value2] +key3: + subkey1: value --- ``` -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 `, 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. - -(syntax/roles/special)= - -(extra-markdown-syntax)= -## Extra markdown syntax - -In addition to roles and directives, MyST supports extra markdown syntax that doesn't -exist in CommonMark. In most cases, these are syntactic short-cuts to calling -roles and directives. We'll cover some common ones below. - -This table describes the rST and MyST equivalents: - -````{list-table} ---- -header-rows: 1 ---- -* - Type - - MyST - - reStructuredText -* - Front matter - - ```md - --- - key: val - --- - ``` - - ```md - :key: val - ``` -* - Comments - - `% comment` - - `.. comment` -* - Targets - - `(mytarget)=` - - `.. _mytarget:` -```` - -(syntax/frontmatter)= - -## Front Matter - -This is a YAML block at the start of the document, as used for example in -[jekyll](https://jekyllrb.com/docs/front-matter/). - - :::{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/)). @@ -429,7 +90,8 @@ This is equivalent to using the [RST `meta` directive](https://www.sphinx-doc.or 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. -:::{tabbed} Sphinx Configuration +::::{tab-set} +:::{tab-item} Sphinx Configuration ```python language = "en" @@ -443,21 +105,22 @@ myst_html_meta = { ::: -:::{tabbed} MyST Front-Matter +:::{tab-item} MyST Front-Matter ```yaml --- -html_meta: - "description lang=en": "metadata description" - "description lang=fr": "description des métadonnées" - "keywords": "Sphinx, MyST" - "property=og:locale": "en_US" +myst: + html_meta: + "description lang=en": "metadata description" + "description lang=fr": "description des métadonnées" + "keywords": "Sphinx, MyST" + "property=og:locale": "en_US" --- ``` ::: -:::{tabbed} RestructuredText +:::{tab-item} RestructuredText ```restructuredtext .. meta:: @@ -469,7 +132,7 @@ html_meta: ::: -:::{tabbed} HTML Output +:::{tab-item} HTML Output ```html @@ -481,7 +144,7 @@ html_meta: ``` ::: - +:::: (syntax/comments)= @@ -516,6 +179,10 @@ a line another line ```` +:::{tip} +Comments are equivalent to the RST syntax: `.. my comment`. +::: + (syntax/blockbreaks)= ## Block Breaks @@ -602,8 +269,7 @@ By default, the reference will use the text of the target (such as the section t {ref}`my text ` ``` -For example, see this ref: {ref}`syntax/targets`, and here's a ref back to the top of -this page: {ref}`my text `. +For example, see this ref: {ref}`syntax/targets`, and here's a ref back to the top of this page: {ref}`my text `. Alternatively using the markdown syntax: @@ -620,7 +286,7 @@ is equivalent to using the [any inline role](https://www.sphinx-doc.org/en/maste 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](example_syntax), and here is a reference to another page (`[](../sphinx/intro.md)`): [](../sphinx/intro.md). +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 @@ -629,7 +295,7 @@ markdown: `[](syntax.md)` will result in: [](syntax.md). ``` (syntax/code-blocks)= -## 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/). diff --git a/example-include.md b/example-include.md index 4d23ee51..3f85dc54 100644 --- a/example-include.md +++ b/example-include.md @@ -1,2 +1,2 @@ -[Used in how-to](docs/sphinx/use.md) +[Used in how-to](docs/faq/index.md) ![alt](docs/_static/logo-wide.svg) diff --git a/myst_parser/__init__.py b/myst_parser/__init__.py index 2be73019..507213b7 100644 --- a/myst_parser/__init__.py +++ b/myst_parser/__init__.py @@ -1,67 +1,10 @@ """An extended commonmark compliant parser, with bridges to docutils & sphinx.""" -from typing import TYPE_CHECKING, Any - __version__ = "0.17.2" -if TYPE_CHECKING: - from sphinx.application import Sphinx - - def setup(app): - """Initialize Sphinx extension.""" - from myst_parser.sphinx_parser import MystParser - - app.add_source_suffix(".md", "markdown") - app.add_source_parser(MystParser) - - setup_sphinx(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} - - -def setup_sphinx(app: "Sphinx"): - """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.directives import FigureMarkdown, SubstitutionReferenceRole - from myst_parser.main import MdParserConfig - from myst_parser.mathjax import override_mathjax - from myst_parser.myst_refs import MystReferenceResolver - - 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.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() diff --git a/myst_parser/_docs.py b/myst_parser/_docs.py new file mode 100644 index 00000000..a7c46a34 --- /dev/null +++ b/myst_parser/_docs.py @@ -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"", 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- [options] [ []]", + ).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 "" diff --git a/myst_parser/cli.py b/myst_parser/cli.py index 267b373c..a9c6ab09 100644 --- a/myst_parser/cli.py +++ b/myst_parser/cli.py @@ -3,7 +3,8 @@ from markdown_it.renderer import RendererHTML -from myst_parser.main import MdParserConfig, create_md_parser +from myst_parser.config.main import MdParserConfig +from myst_parser.parsers.mdit import create_md_parser def print_anchors(args=None): diff --git a/myst_parser/config/__init__.py b/myst_parser/config/__init__.py new file mode 100644 index 00000000..898f9cef --- /dev/null +++ b/myst_parser/config/__init__.py @@ -0,0 +1 @@ +"""This module holds the global configuration for the parser ``MdParserConfig``.""" diff --git a/myst_parser/dc_validators.py b/myst_parser/config/dc_validators.py similarity index 59% rename from myst_parser/dc_validators.py rename to myst_parser/config/dc_validators.py index c0c69ef2..765cfb93 100644 --- a/myst_parser/dc_validators.py +++ b/myst_parser/config/dc_validators.py @@ -2,10 +2,28 @@ from __future__ import annotations import dataclasses as dc -from typing import Any, Callable, Sequence, Type +from typing import Any, Sequence +from typing_extensions import Protocol -def validate_fields(inst): + +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. @@ -15,19 +33,17 @@ def validate_fields(inst): raise an exception if the value is invalid. """ for field in dc.fields(inst): - if "validator" not in field.metadata: - continue - if isinstance(field.metadata["validator"], list): - for validator in field.metadata["validator"]: - validator(inst, field, getattr(inst, field.name)) - else: - field.metadata["validator"](inst, field, getattr(inst, field.name)) + validate_field(inst, field, getattr(inst, field.name)) -ValidatorType = Callable[[Any, dc.Field, Any], None] +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: +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 @@ -36,13 +52,14 @@ def instance_of(type: Type[Any] | tuple[Type[Any], ...]) -> ValidatorType: :param type: The type to check for. """ - def _validator(inst, attr, value): + 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"'{attr.name}' must be {type!r} (got {value!r} that is a {value.__class__!r})." + f"'{field.name}{suffix}' must be of type {type!r} " + f"(got {value!r} that is a {value.__class__!r})." ) return _validator @@ -55,16 +72,16 @@ def optional(validator: ValidatorType) -> ValidatorType: the sub-validator. """ - def _validator(inst, attr, value): + def _validator(inst, field, value, suffix=""): if value is None: return - validator(inst, attr, value) + validator(inst, field, value, suffix=suffix) return _validator -def is_callable(inst, attr, value): +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 @@ -72,7 +89,7 @@ def is_callable(inst, attr, value): """ if not callable(value): raise TypeError( - f"'{attr.name}' must be callable " + f"'{field.name}{suffix}' must be callable " f"(got {value!r} that is a {value.__class__!r})." ) @@ -86,14 +103,16 @@ def in_(options: Sequence) -> ValidatorType: :param options: Allowed options. """ - def _validator(inst, attr, value): + 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"'{attr.name}' must be in {options!r} (got {value!r})") + raise ValueError( + f"'{field.name}{suffix}' must be in {options!r} (got {value!r})" + ) return _validator @@ -108,12 +127,12 @@ def deep_iterable( :param iterable_validator: Validator to apply to iterable itself """ - def _validator(inst, attr, value): + def _validator(inst, field, value, suffix=""): if iterable_validator is not None: - iterable_validator(inst, attr, value) + iterable_validator(inst, field, value, suffix=suffix) - for member in value: - member_validator(inst, attr, member) + for idx, member in enumerate(value): + member_validator(inst, field, member, suffix=f"{suffix}[{idx}]") return _validator @@ -131,12 +150,12 @@ def deep_mapping( :param mapping_validator: Validator to apply to top-level mapping attribute (optional) """ - def _validator(inst, attr, value): + def _validator(inst, field: dc.Field, value, suffix=""): if mapping_validator is not None: - mapping_validator(inst, attr, value) + mapping_validator(inst, field, value) for key in value: - key_validator(inst, attr, key) - value_validator(inst, attr, value[key]) + key_validator(inst, field, key, suffix=f"{suffix}[{key!r}]") + value_validator(inst, field, value[key], suffix=f"{suffix}[{key!r}]") return _validator diff --git a/myst_parser/config/main.py b/myst_parser/config/main.py new file mode 100644 index 00000000..c1f1c7f1 --- /dev/null +++ b/myst_parser/config/main.py @@ -0,0 +1,408 @@ +"""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", + "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 diff --git a/myst_parser/docutils_.py b/myst_parser/docutils_.py index f0a7001f..6f2cc848 100644 --- a/myst_parser/docutils_.py +++ b/myst_parser/docutils_.py @@ -3,266 +3,4 @@ .. include:: path/to/file.md :parser: myst_parser.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 markdown_it.token import Token -from typing_extensions import Literal, get_args, get_origin - -from myst_parser.docutils_renderer import DocutilsRenderer -from myst_parser.main import MdParserConfig, 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": "", "validator": _validate_int}, f"(default: {default})" - if at.type is bool: - return { - "metavar": "", - "validator": frontend.validate_boolean, - }, f"(default: {default})" - if at.type is str: - return { - "metavar": "", - }, 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": "", - "validator": frontend.validate_comma_separated_list, - }, f"(default: '{','.join(default)}')" - if at.type == Tuple[str, str]: - return { - "metavar": "", - "validator": _create_validate_tuple(2), - }, f"(default: '{','.join(default)}')" - if at.type == Union[int, type(None)]: - return { - "metavar": "", - "validator": _validate_int, - }, f"(default: {default})" - if at.type == Union[Iterable[str], type(None)]: - default_str = ",".join(default) if default else "" - return { - "metavar": "", - "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 - try: - config = create_myst_config(document.settings, DOCUTILS_EXCLUDED_ARGS) - except Exception as exc: - error = document.reporter.error(f"myst configuration invalid: {exc}") - document.append(error) - config = MdParserConfig() - - # parse content - parser = create_md_parser(config, DocutilsRenderer) - parser.options["document"] = document - env: dict = {} - tokens = parser.parse(inputstring, env) - if not tokens or tokens[0].type != "front_matter": - # we always add front matter, so that we can merge it with global keys, - # specified in the sphinx configuration - tokens = [Token("front_matter", "", 0, content="{}", map=[0, 0])] + tokens - parser.renderer.render(tokens, parser.options, env) - - # 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) +from myst_parser.parsers.docutils_ import Parser # noqa: F401 diff --git a/myst_parser/main.py b/myst_parser/main.py deleted file mode 100644 index 982fe851..00000000 --- a/myst_parser/main.py +++ /dev/null @@ -1,442 +0,0 @@ -"""This module holds the global configuration for the parser ``MdParserConfig``, -and the ``create_md_parser`` function, which creates a parser from the config. -""" -import dataclasses as dc -from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple, Union, cast - -from markdown_it import MarkdownIt -from markdown_it.renderer import RendererHTML, RendererProtocol -from mdit_py_plugins.amsmath import amsmath_plugin -from mdit_py_plugins.anchors import anchors_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 . import __version__ # noqa: F401 -from .dc_validators import ( - deep_iterable, - deep_mapping, - in_, - instance_of, - is_callable, - optional, - validate_fields, -) - - -def check_extensions(_, __, value): - if not isinstance(value, Iterable): - raise TypeError(f"myst_enable_extensions not iterable: {value}") - diff = set(value).difference( - [ - "dollarmath", - "amsmath", - "deflist", - "fieldlist", - "html_admonition", - "html_image", - "colon_fence", - "smartquotes", - "replacements", - "linkify", - "strikethrough", - "substitution", - "tasklist", - ] - ) - if diff: - raise ValueError(f"myst_enable_extensions 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_`` - """ - - 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 extensions"}, - ) - - linkify_fuzzy_links: bool = dc.field( - default=True, - metadata={ - "validator": instance_of(bool), - "help": "linkify: recognise URLs without schema prefixes", - }, - ) - - dmath_allow_labels: bool = dc.field( - default=True, - metadata={"validator": instance_of(bool), "help": "Parse `$$...$$ (label)`"}, - ) - dmath_allow_space: bool = dc.field( - default=True, - metadata={ - "validator": instance_of(bool), - "help": "dollarmath: allow initial/final spaces in `$ ... $`", - }, - ) - dmath_allow_digits: bool = dc.field( - default=True, - metadata={ - "validator": instance_of(bool), - "help": "dollarmath: allow initial/final digits `1$ ...$2`", - }, - ) - dmath_double_inline: bool = dc.field( - default=False, - metadata={ - "validator": instance_of(bool), - "help": "dollarmath: parse inline `$$ ... $$`", - }, - ) - - update_mathjax: bool = dc.field( - default=True, - metadata={ - "validator": instance_of(bool), - "help": "Update sphinx.ext.mathjax configuration", - }, - ) - - 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", - }, - ) - - disable_syntax: Iterable[str] = dc.field( - default_factory=list, - metadata={ - "validator": deep_iterable(instance_of(str), instance_of((list, tuple))), - "help": "Disable 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 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", - }, - ) - - 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) - ), - "help": "HTML meta tags", - }, - ) - - footnote_transition: bool = dc.field( - default=True, - metadata={ - "validator": instance_of(bool), - "help": "Place a transition before any footnotes", - }, - ) - - 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) - ), - "help": "Substitutions", - }, - ) - - sub_delimiters: Tuple[str, str] = dc.field( - default=("{", "}"), - metadata={"validator": check_sub_delimiters, "help": "Substitution delimiters"}, - ) - - words_per_minute: int = dc.field( - default=200, - metadata={ - "validator": instance_of(int), - "help": "For reading speed calculations", - }, - ) - - def __post_init__(self): - validate_fields(self) - - @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 default_parser(config: MdParserConfig): - raise NotImplementedError( - "default_parser has been deprecated and replaced by create_md_parser." - "You must also supply the renderer class directly to create_md_parser." - ) - - -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 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 - - -def to_docutils( - text: str, - parser_config: Optional[MdParserConfig] = None, - *, - options=None, - env=None, - document=None, - in_sphinx_env: bool = False, - conf=None, - srcdir=None, -): - """Render text to the docutils AST (before transforms) - - :param text: the text to render - :param options: options to update the parser with - :param env: The sandbox environment for the parse - (will contain e.g. reference definitions) - :param document: the docutils root node to use (otherwise a new one will be created) - :param in_sphinx_env: initialise a minimal sphinx environment (useful for testing) - :param conf: the sphinx conf.py as a dictionary - :param srcdir: to parse to the mock sphinx env - - :returns: docutils document - """ - from myst_parser.docutils_renderer import make_document - from myst_parser.sphinx_renderer import SphinxRenderer - - md = create_md_parser(parser_config or MdParserConfig(), SphinxRenderer) - if options: - md.options.update(options) - md.options["document"] = document or make_document() - if in_sphinx_env: - from myst_parser.sphinx_renderer import mock_sphinx_env - - with mock_sphinx_env(conf=conf, srcdir=srcdir, document=md.options["document"]): - return md.render(text, env) - else: - return md.render(text, env) - - -def to_html(text: str, env=None, config: Optional[MdParserConfig] = None): - """Render text to HTML directly using markdown-it-py. - - This is mainly for test purposes only. - """ - config = config or MdParserConfig() - md = create_md_parser(config, RendererHTML) - return md.render(text, env) - - -def to_tokens(text: str, env=None, config: Optional[MdParserConfig] = None): - config = config or MdParserConfig() - md = create_md_parser(config, RendererHTML) - return md.parse(text, env) diff --git a/myst_parser/mdit_to_docutils/__init__.py b/myst_parser/mdit_to_docutils/__init__.py new file mode 100644 index 00000000..0b9307f0 --- /dev/null +++ b/myst_parser/mdit_to_docutils/__init__.py @@ -0,0 +1 @@ +"""Conversion of Markdown-it tokens to docutils AST.""" diff --git a/myst_parser/docutils_renderer.py b/myst_parser/mdit_to_docutils/base.py similarity index 91% rename from myst_parser/docutils_renderer.py rename to myst_parser/mdit_to_docutils/base.py index 4494b549..099b5ad1 100644 --- a/myst_parser/docutils_renderer.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -1,26 +1,15 @@ +"""Convert Markdown-it tokens to docutils nodes.""" +from __future__ import annotations + import inspect import json import os import re from collections import OrderedDict from contextlib import contextmanager -from copy import deepcopy from datetime import date, datetime from types import ModuleType -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Iterator, - List, - MutableMapping, - Optional, - Sequence, - Tuple, - Type, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Iterator, MutableMapping, Sequence, cast from urllib.parse import urlparse import jinja2 @@ -43,7 +32,7 @@ from markdown_it.token import Token from markdown_it.tree import SyntaxTreeNode -from myst_parser.main import MdParserConfig +from myst_parser.config.main import MdParserConfig from myst_parser.mocking import ( MockIncludeDirective, MockingError, @@ -52,8 +41,8 @@ MockState, MockStateMachine, ) +from ..parsers.directives import DirectiveParsingError, parse_directive_text from .html_to_nodes import html_to_nodes -from .parse_directives import DirectiveParsingError, parse_directive_text from .utils import is_external_url if TYPE_CHECKING: @@ -69,7 +58,7 @@ def make_document(source_path="notset", parser_cls=RSTParser) -> nodes.document: REGEX_DIRECTIVE_START = re.compile(r"^[\s]{0,3}([`]{3,10}|[~]{3,10}|[:]{3,10})\{") -def token_line(token: SyntaxTreeNode, default: Optional[int] = None) -> int: +def token_line(token: SyntaxTreeNode, default: int | None = None) -> int: """Retrieve the initial line of a token.""" if not getattr(token, "map", None): if default is not None: @@ -78,6 +67,27 @@ def token_line(token: SyntaxTreeNode, default: Optional[int] = None) -> int: return token.map[0] # type: ignore[index] +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 if it is necessary. + + Note this is overridden in the ``SphinxRenderer``, + to handle suppressed warning types. + """ + kwargs = {"line": line} if line is not None else {} + msg_node = document.reporter.warning(f"{message} [{wtype}.{subtype}]", **kwargs) + if append_to is not None: + append_to.append(msg_node) + return msg_node + + class DocutilsRenderer(RendererProtocol): """A markdown-it-py renderer to populate (in-place) a `docutils.document` AST. @@ -115,7 +125,7 @@ def __getattr__(self, name: str): ) def setup_render( - self, options: Dict[str, Any], env: MutableMapping[str, Any] + self, options: dict[str, Any], env: MutableMapping[str, Any] ) -> None: """Setup the renderer with per render variables.""" self.md_env = env @@ -130,12 +140,12 @@ def setup_render( self.document.settings.language_code ) # a mapping of heading levels to its currently associated node - self._level_to_elem: Dict[int, Union[nodes.document, nodes.section]] = { + self._level_to_elem: dict[int, nodes.document | nodes.section] = { 0: self.document } @property - def sphinx_env(self) -> Optional["BuildEnvironment"]: + def sphinx_env(self) -> BuildEnvironment | None: """Return the sphinx env, if using Sphinx.""" try: return self.document.settings.env @@ -146,23 +156,26 @@ def create_warning( self, message: str, *, - line: Optional[int] = None, - append_to: Optional[nodes.Element] = None, + line: int | None = None, + append_to: nodes.Element | None = None, wtype: str = "myst", subtype: str = "other", - ) -> Optional[nodes.system_message]: + ) -> nodes.system_message | None: """Generate a warning, logging if it is necessary. Note this is overridden in the ``SphinxRenderer``, to handle suppressed warning types. """ - kwargs = {"line": line} if line is not None else {} - msg_node = self.reporter.warning(f"{message} [{wtype}.{subtype}]", **kwargs) - if append_to is not None: - append_to.append(msg_node) - return msg_node + return create_warning( + self.document, + message, + line=line, + append_to=append_to, + wtype=wtype, + subtype=subtype, + ) - def _render_tokens(self, tokens: List[Token]) -> None: + def _render_tokens(self, tokens: list[Token]) -> None: """Render the tokens.""" # propagate line number down to inline elements for token in tokens: @@ -213,8 +226,24 @@ def render( containing additional metadata like reference info """ self.setup_render(options, md_env) - + self._render_initialise() self._render_tokens(list(tokens)) + self._render_finalise() + return self.document + + def _render_initialise(self) -> None: + """Initialise the render of the document.""" + self.current_node.extend( + html_meta_to_nodes( + self.md_config.html_meta, + document=self.document, + line=0, + reporter=self.reporter, + ) + ) + + def _render_finalise(self) -> None: + """Finalise the render of the document.""" # log warnings for duplicate reference definitions # "duplicate_refs": [{"href": "ijk", "label": "B", "map": [4, 5], "title": ""}], @@ -255,34 +284,29 @@ def render( else: self.render_footnote_reference(foot_ref_tokens[0]) - self.add_document_wordcount() - - return self.document - - def add_document_wordcount(self) -> None: - """Add the wordcount, generated by the ``mdit_py_plugins.wordcount_plugin``.""" - + # Add the wordcount, generated by the ``mdit_py_plugins.wordcount_plugin``. wordcount_metadata = self.md_env.get("wordcount", {}) - if not wordcount_metadata: - return - - # save the wordcount to the sphinx BuildEnvironment metadata - if self.sphinx_env is not None: - meta = self.sphinx_env.metadata.setdefault(self.sphinx_env.docname, {}) - meta["wordcount"] = wordcount_metadata - - # now add the wordcount as substitution definitions, - # so we can reference them in the document - for key in ("words", "minutes"): - value = wordcount_metadata.get(key, None) - if value is None: - continue - substitution_node = nodes.substitution_definition( - str(value), nodes.Text(str(value)) - ) - substitution_node.source = self.document["source"] - substitution_node["names"].append(f"wordcount-{key}") - self.document.note_substitution_def(substitution_node, f"wordcount-{key}") + if wordcount_metadata: + + # save the wordcount to the sphinx BuildEnvironment metadata + if self.sphinx_env is not None: + meta = self.sphinx_env.metadata.setdefault(self.sphinx_env.docname, {}) + meta["wordcount"] = wordcount_metadata + + # now add the wordcount as substitution definitions, + # so we can reference them in the document + for key in ("words", "minutes"): + value = wordcount_metadata.get(key, None) + if value is None: + continue + substitution_node = nodes.substitution_definition( + str(value), nodes.Text(str(value)) + ) + substitution_node.source = self.document["source"] + substitution_node["names"].append(f"wordcount-{key}") + self.document.note_substitution_def( + substitution_node, f"wordcount-{key}" + ) def nested_render_text( self, text: str, lineno: int, inline: bool = False, allow_headings: bool = True @@ -349,7 +373,7 @@ def add_line_and_source_path(self, node, token: SyntaxTreeNode) -> None: node.source = self.document["source"] def add_line_and_source_path_r( - self, nodes: List[nodes.Element], token: SyntaxTreeNode + self, nodes: list[nodes.Element], token: SyntaxTreeNode ) -> None: """Copy the line number and document source path to the docutils nodes, and recursively to all descendants. @@ -394,7 +418,7 @@ def update_section_level_state(self, section: nodes.section, level: int) -> None if section_level <= level } - def renderInlineAsText(self, tokens: List[SyntaxTreeNode]) -> str: + def renderInlineAsText(self, tokens: list[SyntaxTreeNode]) -> str: """Special kludge for image `alt` attributes to conform CommonMark spec. Don't try to use it! Spec requires to show `alt` content with stripped markup, @@ -492,12 +516,12 @@ def render_code_inline(self, token: SyntaxTreeNode) -> None: def create_highlighted_code_block( self, text: str, - lexer_name: Optional[str], + lexer_name: str | None, number_lines: bool = False, lineno_start: int = 1, - source: Optional[str] = None, - line: Optional[int] = None, - node_cls: Type[nodes.Element] = nodes.literal_block, + source: str | None = None, + line: int | None = None, + node_cls: type[nodes.Element] = nodes.literal_block, ) -> nodes.Element: """Create a literal block with syntax highlighting. @@ -768,66 +792,45 @@ def render_front_matter(self, token: SyntaxTreeNode) -> None: """Pass document front matter data.""" position = token_line(token, default=0) - if not isinstance(token.content, dict): + if isinstance(token.content, str): try: data = yaml.safe_load(token.content) - assert isinstance(data, dict), "not dict" - except ( - AssertionError, - yaml.parser.ParserError, - yaml.scanner.ScannerError, - ) as error: - msg_node = self.reporter.error( - "Front matter block:\n" + str(error), line=position + except (yaml.parser.ParserError, yaml.scanner.ScannerError): + self.create_warning( + "Malformed YAML", + line=position, + append_to=self.current_node, + subtype="topmatter", ) - msg_node += nodes.literal_block(token.content, token.content) - self.current_node.append(msg_node) return else: - data = deepcopy(token.content) + data = token.content - substitutions = data.pop("substitutions", {}) - html_meta = data.pop("html_meta", {}) + if not isinstance(data, dict): + self.create_warning( + f"YAML is not a dict: {type(data)}", + line=position, + append_to=self.current_node, + subtype="topmatter", + ) + return - if data: + fields = { + k: v + for k, v in data.items() + if k not in ("myst", "mystnb", "substitutions", "html_meta") + } + if fields: field_list = self.dict_to_fm_field_list( - data, language_code=self.document.settings.language_code + fields, language_code=self.document.settings.language_code ) self.current_node.append(field_list) - if isinstance(substitutions, dict): - self.document.fm_substitutions = substitutions - else: - msg_node = self.reporter.error( - "Front-matter 'substitutions' is not a dict", line=position - ) - msg_node += nodes.literal_block(token.content, token.content) - self.current_node.append(msg_node) - - if not isinstance(html_meta, dict): - msg_node = self.reporter.error( - "Front-matter 'html_meta' is not a dict", line=position - ) - msg_node += nodes.literal_block(token.content, token.content) - self.current_node.append(msg_node) - - self.current_node.extend( - html_meta_to_nodes( - { - **self.md_config.html_meta, - **html_meta, - }, - document=self.document, - line=position, - reporter=self.reporter, - ) - ) - if data.get("title") and self.md_config.title_to_header: self.nested_render_text(f"# {data['title']}", 0) def dict_to_fm_field_list( - self, data: Dict[str, Any], language_code: str, line: int = 0 + self, data: dict[str, Any], language_code: str, line: int = 0 ) -> nodes.field_list: """Render each key/val pair as a docutils ``field_node``. @@ -1004,7 +1007,7 @@ def render_footnote_ref(self, token: SyntaxTreeNode) -> None: """ target = token.meta["label"] - refnode = nodes.footnote_reference("[^{}]".format(target)) + refnode = nodes.footnote_reference(f"[^{target}]") self.add_line_and_source_path(refnode, token) if not target.isdigit(): refnode["auto"] = 1 @@ -1066,7 +1069,7 @@ def render_myst_role(self, token: SyntaxTreeNode) -> None: self.current_node += nodes else: message = self.reporter.error( - 'Unknown interpreted text role "{}".'.format(name), line=lineno + f'Unknown interpreted text role "{name}".', line=lineno ) problematic = inliner.problematic(text, rawsource, message) self.current_node += problematic @@ -1192,7 +1195,7 @@ def render_directive(self, token: SyntaxTreeNode) -> None: def run_directive( self, name: str, first_line: str, content: str, position: int - ) -> List[nodes.Element]: + ) -> list[nodes.Element]: """Run a directive and return the generated nodes. :param name: the name of the directive @@ -1207,13 +1210,13 @@ def run_directive( self.document.current_line = position # get directive class - output: Tuple[Directive, list] = directives.directive( + output: tuple[Directive, list] = directives.directive( name, self.language_module_rst, self.document ) directive_class, messages = output if not directive_class: error = self.reporter.error( - 'Unknown directive type "{}".\n'.format(name), + f'Unknown directive type "{name}".\n', # nodes.literal_block(content, content), line=position, ) @@ -1231,7 +1234,7 @@ def run_directive( ) except DirectiveParsingError as error: error = self.reporter.error( - "Directive '{}': {}".format(name, error), + f"Directive '{name}': {error}", nodes.literal_block(content, content), line=position, ) @@ -1290,7 +1293,7 @@ def run_directive( assert isinstance( result, list - ), 'Directive "{}" must return a list of nodes.'.format(name) + ), f'Directive "{name}" must return a list of nodes.' for i in range(len(result)): assert isinstance( result[i], nodes.Node @@ -1322,10 +1325,7 @@ def render_substitution(self, token: SyntaxTreeNode, inline: bool) -> None: position = token_line(token) # front-matter substitutions take priority over config ones - variable_context: Dict[str, Any] = { - **self.md_config.substitutions, - **getattr(self.document, "fm_substitutions", {}), - } + variable_context: dict[str, Any] = {**self.md_config.substitutions} if self.sphinx_env is not None: variable_context["env"] = self.sphinx_env @@ -1378,8 +1378,8 @@ def render_substitution(self, token: SyntaxTreeNode, inline: bool) -> None: def html_meta_to_nodes( - data: Dict[str, Any], document: nodes.document, line: int, reporter: Reporter -) -> List[Union[nodes.pending, nodes.system_message]]: + data: dict[str, Any], document: nodes.document, line: int, reporter: Reporter +) -> list[nodes.pending | nodes.system_message]: """Replicate the `meta` directive, by converting a dictionary to a list of pending meta nodes diff --git a/myst_parser/html_to_nodes.py b/myst_parser/mdit_to_docutils/html_to_nodes.py similarity index 92% rename from myst_parser/html_to_nodes.py rename to myst_parser/mdit_to_docutils/html_to_nodes.py index 5d05ea0d..2cc30667 100644 --- a/myst_parser/html_to_nodes.py +++ b/myst_parser/mdit_to_docutils/html_to_nodes.py @@ -1,13 +1,15 @@ """Convert HTML to docutils nodes.""" +from __future__ import annotations + import re -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from docutils import nodes -from .parse_html import Data, tokenize_html +from myst_parser.parsers.parse_html import Data, tokenize_html if TYPE_CHECKING: - from .docutils_renderer import DocutilsRenderer + from .base import DocutilsRenderer def make_error( @@ -32,7 +34,7 @@ def make_error( ) -def default_html(text: str, source: str, line_number: int) -> List[nodes.Element]: +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 @@ -40,8 +42,8 @@ def default_html(text: str, source: str, line_number: int) -> List[nodes.Element def html_to_nodes( - text: str, line_number: int, renderer: "DocutilsRenderer" -) -> List[nodes.Element]: + 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("<", "<"), text) diff --git a/myst_parser/sphinx_renderer.py b/myst_parser/mdit_to_docutils/sphinx_.py similarity index 58% rename from myst_parser/sphinx_renderer.py rename to myst_parser/mdit_to_docutils/sphinx_.py index 379fbb94..3c1bc237 100644 --- a/myst_parser/sphinx_renderer.py +++ b/myst_parser/mdit_to_docutils/sphinx_.py @@ -1,35 +1,55 @@ -import copy +"""Convert Markdown-it tokens to docutils nodes, including sphinx specific elements.""" +from __future__ import annotations + import os -import tempfile -from contextlib import contextmanager -from io import StringIO from pathlib import Path -from typing import Optional, cast +from typing import cast from urllib.parse import unquote from uuid import uuid4 from docutils import nodes -from docutils.parsers.rst import directives, roles from markdown_it.tree import SyntaxTreeNode from sphinx import addnodes -from sphinx.application import Sphinx, builtin_extensions -from sphinx.config import Config from sphinx.domains.math import MathDomain from sphinx.domains.std import StandardDomain from sphinx.environment import BuildEnvironment -from sphinx.events import EventManager -from sphinx.project import Project -from sphinx.registry import SphinxComponentRegistry from sphinx.util import logging -from sphinx.util.docutils import additional_nodes, sphinx_domains, unregister_node from sphinx.util.nodes import clean_astext -from sphinx.util.tags import Tags -from myst_parser.docutils_renderer import DocutilsRenderer +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. @@ -45,26 +65,24 @@ def create_warning( self, message: str, *, - line: Optional[int] = None, - append_to: Optional[nodes.Element] = None, + line: int | None = None, + append_to: nodes.Element | None = None, wtype: str = "myst", subtype: str = "other", - ) -> Optional[nodes.system_message]: + ) -> 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 not logging.is_suppressed_warning( - wtype, subtype, self.doc_env.app.config.suppress_warnings - ): - msg_node = self.reporter.warning(message, **kwargs) - if append_to is not None: - append_to.append(msg_node) - return None + 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")`, @@ -225,114 +243,3 @@ def add_math_target(self, node: nodes.math_block) -> nodes.target: target = nodes.target("", "", ids=[node_id]) self.document.note_explicit_target(target) return target - - -def minimal_sphinx_app( - configuration=None, sourcedir=None, with_builder=False, raise_on_warning=False -): - """Create a minimal Sphinx environment; loading sphinx roles, directives, etc.""" - - class MockSphinx(Sphinx): - """Minimal sphinx init to load roles and directives.""" - - def __init__(self, confoverrides=None, srcdir=None, raise_on_warning=False): - self.extensions = {} - self.registry = SphinxComponentRegistry() - try: - self.html_themes = {} - except AttributeError: - # changed to property in sphinx 4.1 - pass - self.events = EventManager(self) - - # logging - self.verbosity = 0 - self._warncount = 0 - self.warningiserror = raise_on_warning - self._status = StringIO() - self._warning = StringIO() - logging.setup(self, self._status, self._warning) - - self.tags = Tags([]) - self.config = Config({}, confoverrides or {}) - self.config.pre_init_values() - self._init_i18n() - for extension in builtin_extensions: - self.registry.load_extension(self, extension) - # fresh env - self.doctreedir = "" - self.srcdir = srcdir - self.confdir = None - self.outdir = "" - self.project = Project(srcdir=srcdir, source_suffix={".md": "markdown"}) - self.project.docnames = {"mock_docname"} - self.env = BuildEnvironment(self) - self.env.setup(self) - self.env.temp_data["docname"] = "mock_docname" - # Ignore type checkers because we disrespect superclass typing here - self.builder = None # type: ignore[assignment] - - if not with_builder: - return - - # this code is only required for more complex parsing with extensions - for extension in self.config.extensions: - self.setup_extension(extension) - buildername = "dummy" - self.preload_builder(buildername) - self.config.init_values() - self.events.emit("config-inited", self.config) - - with tempfile.TemporaryDirectory() as tempdir: - # creating a builder attempts to make the doctreedir - self.doctreedir = tempdir - self.builder = self.create_builder(buildername) - self.doctreedir = "" - - app = MockSphinx( - confoverrides=configuration, srcdir=sourcedir, raise_on_warning=raise_on_warning - ) - return app - - -@contextmanager -def mock_sphinx_env( - conf=None, srcdir=None, document=None, with_builder=False, raise_on_warning=False -): - """Set up an environment, to parse sphinx roles/directives, - outside of a `sphinx-build`. - - :param conf: a dictionary representation of the sphinx `conf.py` - :param srcdir: a path to a source directory - (for example, can be used for `include` statements) - - This primarily copies the code in `sphinx.util.docutils.docutils_namespace` - and `sphinx.util.docutils.sphinx_domains`. - """ - # store currently loaded roles/directives, so we can revert on exit - _directives = copy.copy(directives._directives) - _roles = copy.copy(roles._roles) - # Monkey-patch directive and role dispatch, - # so that sphinx domain-specific markup takes precedence. - app = minimal_sphinx_app( - configuration=conf, - sourcedir=srcdir, - with_builder=with_builder, - raise_on_warning=raise_on_warning, - ) - _sphinx_domains = sphinx_domains(app.env) - _sphinx_domains.enable() - if document is not None: - document.settings.env = app.env - try: - yield app - finally: - # revert loaded roles/directives - directives._directives = _directives - roles._roles = _roles - # TODO unregister nodes (see `sphinx.util.docutils.docutils_namespace`) - for node in list(additional_nodes): - unregister_node(node) - additional_nodes.discard(node) - # revert directive/role function (see `sphinx.util.docutils.sphinx_domains`) - _sphinx_domains.disable() diff --git a/myst_parser/utils.py b/myst_parser/mdit_to_docutils/utils.py similarity index 100% rename from myst_parser/utils.py rename to myst_parser/mdit_to_docutils/utils.py diff --git a/myst_parser/mocking.py b/myst_parser/mocking.py index 15786a50..b22475d8 100644 --- a/myst_parser/mocking.py +++ b/myst_parser/mocking.py @@ -1,11 +1,13 @@ """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, List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Any from docutils import nodes from docutils.parsers.rst import Directive, DirectiveError @@ -15,10 +17,10 @@ from docutils.statemachine import StringList from docutils.utils import unescape -from .parse_directives import parse_directive_text +from .parsers.directives import parse_directive_text if TYPE_CHECKING: - from .docutils_renderer import DocutilsRenderer + from .mdit_to_docutils.base import DocutilsRenderer class MockingError(Exception): @@ -31,7 +33,7 @@ class MockInliner: This is parsed to role functions. """ - def __init__(self, renderer: "DocutilsRenderer"): + def __init__(self, renderer: DocutilsRenderer): """Initialize the mock inliner.""" self._renderer = renderer # here we mock that the `parse` method has already been called @@ -59,7 +61,7 @@ def problematic( def parse( self, text: str, lineno: int, memo: Any, parent: nodes.Node - ) -> Tuple[List[nodes.Node], List[nodes.system_message]]: + ) -> 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`, @@ -86,7 +88,7 @@ def __getattr__(self, name: str): cls=type(self).__name__, name=name ) raise MockingError(msg).with_traceback(sys.exc_info()[2]) - msg = "{cls} has no attribute {name}".format(cls=type(self).__name__, name=name) + msg = f"{type(self).__name__} has no attribute {name}" raise MockingError(msg).with_traceback(sys.exc_info()[2]) @@ -100,8 +102,8 @@ class MockState: def __init__( self, - renderer: "DocutilsRenderer", - state_machine: "MockStateMachine", + renderer: DocutilsRenderer, + state_machine: MockStateMachine, lineno: int, ): self._renderer = renderer @@ -115,7 +117,7 @@ class Struct: document = self.document reporter = self.document.reporter language = renderer.language_module_rst - title_styles: List[str] = [] + title_styles: list[str] = [] section_level = max(renderer._level_to_elem) section_bubble_up_kludge = False inliner = self.inliner @@ -126,9 +128,9 @@ def parse_directive_block( self, content: StringList, line_offset: int, - directive: Type[Directive], + directive: type[Directive], option_presets: dict, - ) -> Tuple[list, dict, StringList, int]: + ) -> tuple[list, dict, StringList, int]: """Parse the full directive text :returns: (arguments, options, content, content_offset) @@ -189,7 +191,7 @@ def parse_target(self, block, block_text, lineno: int): def inline_text( self, text: str, lineno: int - ) -> Tuple[List[nodes.Element], List[nodes.Element]]: + ) -> tuple[list[nodes.Element], list[nodes.Element]]: """Parse text with only inline rules. :returns: (list of nodes, list of messages) @@ -199,7 +201,7 @@ def inline_text( # U+2014 is an em-dash: attribution_pattern = re.compile("^((?:---?(?!-)|\u2014) *)(.+)") - def block_quote(self, lines: List[str], line_offset: int) -> List[nodes.Element]: + 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. @@ -284,7 +286,7 @@ class MockStateMachine: This is parsed to the `Directives.run()` method. """ - def __init__(self, renderer: "DocutilsRenderer", lineno: int): + def __init__(self, renderer: DocutilsRenderer, lineno: int): self._renderer = renderer self._lineno = lineno self.document = renderer.document @@ -293,11 +295,11 @@ def __init__(self, renderer: "DocutilsRenderer", lineno: int): self.node: nodes.Element = renderer.current_node self.match_titles: bool = True - def get_source(self, lineno: Optional[int] = None): + def get_source(self, lineno: int | None = None): """Return document source path.""" return self.document["source"] - def get_source_and_line(self, lineno: Optional[int] = None): + 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 @@ -310,7 +312,7 @@ def __getattr__(self, name: str): cls=type(self).__name__, name=name ) raise MockingError(msg).with_traceback(sys.exc_info()[2]) - msg = "{cls} has no attribute {name}".format(cls=type(self).__name__, name=name) + msg = f"{type(self).__name__} has no attribute {name}" raise MockingError(msg).with_traceback(sys.exc_info()[2]) @@ -324,12 +326,12 @@ class MockIncludeDirective: def __init__( self, - renderer: "DocutilsRenderer", + renderer: DocutilsRenderer, name: str, klass: Include, arguments: list, options: dict, - body: List[str], + body: list[str], lineno: int, ): self.renderer = renderer @@ -341,12 +343,12 @@ def __init__( self.body = body self.lineno = lineno - def run(self) -> List[nodes.Element]: + 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, 'Directive "{}" disabled.'.format(self.name)) + 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()]) diff --git a/myst_parser/parsers/__init__.py b/myst_parser/parsers/__init__.py new file mode 100644 index 00000000..26fbfcab --- /dev/null +++ b/myst_parser/parsers/__init__.py @@ -0,0 +1 @@ +"""Parsers of MyST Markdown source text to docutils AST.""" diff --git a/myst_parser/parse_directives.py b/myst_parser/parsers/directives.py similarity index 94% rename from myst_parser/parse_directives.py rename to myst_parser/parsers/directives.py index b053deae..e275c843 100644 --- a/myst_parser/parse_directives.py +++ b/myst_parser/parsers/directives.py @@ -33,10 +33,12 @@ 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, Dict, List, Tuple, Type +from typing import Any, Callable import yaml from docutils.parsers.rst import Directive @@ -50,11 +52,11 @@ class DirectiveParsingError(Exception): def parse_directive_text( - directive_class: Type[Directive], + directive_class: type[Directive], first_line: str, content: str, validate_options: bool = True, -) -> Tuple[List[str], dict, List[str], int]: +) -> 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. @@ -103,10 +105,10 @@ def parse_directive_text( def parse_directive_options( - content: str, directive_class: Type[Directive], validate: bool = True + content: str, directive_class: type[Directive], validate: bool = True ): """Parse (and validate) the directive option section.""" - options: Dict[str, Any] = {} + options: dict[str, Any] = {} if content.startswith("---"): content = "\n".join(content.splitlines()[1:]) match = re.search(r"^-{3,}", content, re.MULTILINE) @@ -143,7 +145,7 @@ def parse_directive_options( return content, options # check options against spec - options_spec = directive_class.option_spec # type: Dict[str, Callable] + options_spec: dict[str, Callable] = directive_class.option_spec for name, value in list(options.items()): try: convertor = options_spec[name] @@ -179,7 +181,7 @@ def parse_directive_arguments(directive, arg_text): arguments = arg_text.split() if len(arguments) < required: raise DirectiveParsingError( - "{} argument(s) required, {} supplied".format(required, len(arguments)) + f"{required} argument(s) required, {len(arguments)} supplied" ) elif len(arguments) > required + optional: if directive.final_argument_whitespace: diff --git a/myst_parser/parsers/docutils_.py b/myst_parser/parsers/docutils_.py new file mode 100644 index 00000000..aaef5e2f --- /dev/null +++ b/myst_parser/parsers/docutils_.py @@ -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": "", "validator": _validate_int}, f"(default: {default})" + if at.type is bool: + return { + "metavar": "", + "validator": frontend.validate_boolean, + }, f"(default: {default})" + if at.type is str: + return { + "metavar": "", + }, 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": "", + "validator": frontend.validate_comma_separated_list, + }, f"(default: '{','.join(default)}')" + if at.type == Tuple[str, str]: + return { + "metavar": "", + "validator": _create_validate_tuple(2), + }, f"(default: '{','.join(default)}')" + if at.type == Union[int, type(None)]: + return { + "metavar": "", + "validator": _validate_int, + }, f"(default: {default})" + if at.type == Union[Iterable[str], type(None)]: + default_str = ",".join(default) if default else "" + return { + "metavar": "", + "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) diff --git a/myst_parser/parsers/mdit.py b/myst_parser/parsers/mdit.py new file mode 100644 index 00000000..249acd68 --- /dev/null +++ b/myst_parser/parsers/mdit.py @@ -0,0 +1,120 @@ +"""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.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 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 diff --git a/myst_parser/parse_html.py b/myst_parser/parsers/parse_html.py similarity index 89% rename from myst_parser/parse_html.py rename to myst_parser/parsers/parse_html.py index d881f7fd..7539e42f 100644 --- a/myst_parser/parse_html.py +++ b/myst_parser/parsers/parse_html.py @@ -17,11 +17,13 @@ (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, Dict, Iterable, Iterator, List, Optional, Type, Union +from typing import Any, Callable, Iterable, Iterator class Attribute(dict): @@ -32,7 +34,7 @@ def __getitem__(self, key: str) -> str: return self.get(key, "") @property - def classes(self) -> List[str]: + def classes(self) -> list[str]: """Return 'class' attribute as list.""" return self["class"].split() @@ -47,24 +49,24 @@ class Element(abc.MutableSequence): All xml/html entities inherit from this class. """ - def __init__(self, name: str = "", attr: Optional[dict] = None) -> None: + def __init__(self, name: str = "", attr: dict | None = None) -> None: """Initialise the element.""" self.name = name self.attrs: Attribute = Attribute(attr or {}) - self._parent: Optional[Element] = None - self._children: List[Element] = [] + self._parent: Element | None = None + self._children: list[Element] = [] @property - def parent(self) -> Optional["Element"]: + def parent(self) -> Element | None: """Return parent.""" return self._parent @property - def children(self) -> List["Element"]: + def children(self) -> list[Element]: """Return copy of children.""" return self._children[:] - def reset_children(self, children: List["Element"], deepcopy: bool = False): + def reset_children(self, children: list[Element], deepcopy: bool = False): new_children = [] for i, item in enumerate(children): assert isinstance(item, Element) @@ -77,10 +79,10 @@ def reset_children(self, children: List["Element"], deepcopy: bool = False): new_children.append(item) self._children = new_children - def __getitem__(self, index: int) -> "Element": # type: ignore[override] + def __getitem__(self, index: int) -> Element: # type: ignore[override] return self._children[index] - def __setitem__(self, index: int, item: "Element"): # type: ignore[override] + 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}") @@ -93,18 +95,17 @@ def __delitem__(self, index: int): # type: ignore[override] def __len__(self) -> int: return self._children.__len__() - def __iter__(self) -> Iterator["Element"]: - for child in self._children: - yield child + def __iter__(self) -> Iterator[Element]: + yield from self._children - def insert(self, index: int, item: "Element"): + 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": + def deepcopy(self) -> Element: """Recursively copy and remove parent.""" _copy = self.__class__(self.name, self.attrs) for child in self: @@ -121,7 +122,7 @@ def __repr__(self) -> str: def render( self, - tag_overrides: Optional[Dict[str, Callable[["Element", dict], str]]] = None, + tag_overrides: dict[str, Callable[[Element, dict], str]] | None = None, **kwargs, ) -> str: """Returns a HTML string representation of the element. @@ -138,16 +139,15 @@ def __str__(self) -> str: def __eq__(self, item: Any) -> bool: return item is self - def walk(self, include_self: bool = False) -> Iterator["Element"]: + 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 - for ancestor in child.walk(): - yield ancestor + yield from child.walk() - def strip(self, inplace: bool = False, recurse: bool = False) -> "Element": + def strip(self, inplace: bool = False, recurse: bool = False) -> Element: """Return copy with all `Data` tokens that only contain whitespace / newlines removed. """ @@ -168,12 +168,12 @@ def strip(self, inplace: bool = False, recurse: bool = False) -> "Element": def find( self, - identifier: Union[str, Type["Element"]], - attrs: Optional[dict] = None, - classes: Optional[Iterable[str]] = None, + identifier: str | type[Element], + attrs: dict | None = None, + classes: Iterable[str] | None = None, include_self: bool = False, recurse: bool = True, - ) -> Iterator["Element"]: + ) -> Iterator[Element]: """Find all elements that match name and specific attributes.""" iterator = self.walk() if recurse else self if include_self: @@ -207,7 +207,7 @@ class Tag(Element): def render( self, - tag_overrides: Optional[Dict[str, Callable[[Element, dict], str]]] = None, + tag_overrides: dict[str, Callable[[Element, dict], str]] | None = None, **kwargs, ) -> str: if tag_overrides and self.name in tag_overrides: @@ -226,7 +226,7 @@ class XTag(Element): def render( self, - tag_overrides: Optional[Dict[str, Callable[[Element, dict], str]]] = None, + tag_overrides: dict[str, Callable[[Element, dict], str]] | None = None, **kwargs, ) -> str: if tag_overrides is not None and self.name in tag_overrides: @@ -252,7 +252,7 @@ def __repr__(self) -> str: text = text[:17] + "..." return f"{self.__class__.__name__}({text!r})" - def deepcopy(self) -> "TerminalElement": + def deepcopy(self) -> TerminalElement: """Copy and remove parent.""" _copy = self.__class__(self.data) return _copy @@ -300,7 +300,7 @@ def render(self, **kwargs) -> str: # type: ignore[override] return f"&{self.data};" -class Tree(object): +class Tree: """The engine class to generate the AST tree.""" def __init__(self, name: str = ""): @@ -342,7 +342,7 @@ def nest_vtag(self, name: str, attrs: dict): item = VoidTag(name, attrs) top.append(item) - def nest_terminal(self, klass: Type[TerminalElement], data: str): + def nest_terminal(self, klass: type[TerminalElement], data: str): """Nest the data onto the tree.""" top = self.last() item = klass(data) diff --git a/myst_parser/parsers/sphinx_.py b/myst_parser/parsers/sphinx_.py new file mode 100644 index 00000000..fff098f3 --- /dev/null +++ b/myst_parser/parsers/sphinx_.py @@ -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) diff --git a/myst_parser/sphinx_.py b/myst_parser/sphinx_.py index e7979c80..b0850865 100644 --- a/myst_parser/sphinx_.py +++ b/myst_parser/sphinx_.py @@ -3,4 +3,4 @@ .. include:: path/to/file.md :parser: myst_parser.sphinx_ """ -from myst_parser.sphinx_parser import MystParser as Parser # noqa: F401 +from myst_parser.parsers.sphinx_ import MystParser as Parser # noqa: F401 diff --git a/myst_parser/sphinx_ext/__init__.py b/myst_parser/sphinx_ext/__init__.py new file mode 100644 index 00000000..1bfeb71a --- /dev/null +++ b/myst_parser/sphinx_ext/__init__.py @@ -0,0 +1 @@ +"""Sphinx extension for myst_parser.""" diff --git a/myst_parser/directives.py b/myst_parser/sphinx_ext/directives.py similarity index 98% rename from myst_parser/directives.py rename to myst_parser/sphinx_ext/directives.py index 4cb90306..39ca2c65 100644 --- a/myst_parser/directives.py +++ b/myst_parser/sphinx_ext/directives.py @@ -29,7 +29,7 @@ class SubstitutionReferenceRole(SphinxRole): 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) # type: ignore[arg-type] + self.set_source_info(subref_node, self.lineno) subref_node["refname"] = nodes.fully_normalize_name(self.text) return [subref_node], [] diff --git a/myst_parser/sphinx_ext/main.py b/myst_parser/sphinx_ext/main.py new file mode 100644 index 00000000..f5aeffc1 --- /dev/null +++ b/myst_parser/sphinx_ext/main.py @@ -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() diff --git a/myst_parser/mathjax.py b/myst_parser/sphinx_ext/mathjax.py similarity index 97% rename from myst_parser/mathjax.py rename to myst_parser/sphinx_ext/mathjax.py index e430c3ba..260f0080 100644 --- a/myst_parser/mathjax.py +++ b/myst_parser/sphinx_ext/mathjax.py @@ -53,14 +53,14 @@ def override_mathjax(app: Sphinx): if "dollarmath" not in app.config["myst_enable_extensions"]: return - if not app.env.myst_config.update_mathjax: # type: ignore[attr-defined] + if not app.env.myst_config.update_mathjax: # type: ignore return - mjax_classes = app.env.myst_config.mathjax_classes # type: ignore[attr-defined] + 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[attr-defined] + 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"] diff --git a/myst_parser/myst_refs.py b/myst_parser/sphinx_ext/myst_refs.py similarity index 99% rename from myst_parser/myst_refs.py rename to myst_parser/sphinx_ext/myst_refs.py index e7847957..81675a1b 100644 --- a/myst_parser/myst_refs.py +++ b/myst_parser/sphinx_ext/myst_refs.py @@ -117,6 +117,8 @@ def resolve_myst_ref( # 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")) @@ -246,6 +248,7 @@ def _resolve_ref_nested( if not docname: return None + assert self.app.builder return make_refnode(self.app.builder, fromdocname, docname, labelid, innernode) def _resolve_doc_nested( @@ -277,4 +280,5 @@ def _resolve_doc_nested( 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) diff --git a/myst_parser/sphinx_parser.py b/myst_parser/sphinx_parser.py deleted file mode 100644 index 7799d498..00000000 --- a/myst_parser/sphinx_parser.py +++ /dev/null @@ -1,80 +0,0 @@ -import time -from os import path -from typing import Tuple - -from docutils import nodes -from docutils.core import publish_doctree -from docutils.parsers.rst import Parser as RstParser -from markdown_it.token import Token -from sphinx.application import Sphinx -from sphinx.io import SphinxStandaloneReader -from sphinx.parsers import Parser as SphinxParser -from sphinx.util import logging -from sphinx.util.docutils import sphinx_domains - -from myst_parser.main import create_md_parser -from myst_parser.sphinx_renderer import SphinxRenderer - -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 - - """ - config = document.settings.env.myst_config - parser = create_md_parser(config, SphinxRenderer) - parser.options["document"] = document - env: dict = {} - tokens = parser.parse(inputstring, env) - if not tokens or tokens[0].type != "front_matter": - # we always add front matter, so that we can merge it with global keys, - # specified in the sphinx configuration - tokens = [Token("front_matter", "", 0, content="{}", map=[0, 0])] + tokens - parser.renderer.render(tokens, parser.options, env) - - -def parse(app: Sphinx, text: str, docname: str = "index") -> nodes.document: - """Parse a string as MystMarkdown with Sphinx application.""" - app.env.temp_data["docname"] = docname - app.env.all_docs[docname] = time.time() - reader = SphinxStandaloneReader() - reader.setup(app) - parser = MystParser() - parser.set_application(app) - with sphinx_domains(app.env): - return publish_doctree( - text, - path.join(app.srcdir, docname + ".md"), - reader=reader, - parser=parser, - parser_name="markdown", - settings_overrides={"env": app.env, "gettext_compact": True}, - ) diff --git a/pyproject.toml b/pyproject.toml index 5a36d88b..487d334e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,29 +55,29 @@ linkify = ["linkify-it-py~=1.0"] rtd = [ "ipython", "sphinx-book-theme", - "sphinx-panels", - "sphinxcontrib-bibtex~=2.4", + "sphinx-design", "sphinxext-rediraffe~=0.2.7", "sphinxcontrib.mermaid~=0.7.1", "sphinxext-opengraph~=0.6.3", ] testing = [ "beautifulsoup4", - "coverage", + "coverage[toml]", "docutils~=0.17.0", # this version changes some HTML tags "pytest>=6,<7", "pytest-cov", "pytest-regressions", "pytest-param-files~=0.3.4", + "sphinx-pytest", ] [project.scripts] myst-anchors = "myst_parser.cli:print_anchors" -myst-docutils-html = "myst_parser.docutils_:cli_html" -myst-docutils-html5 = "myst_parser.docutils_:cli_html5" -myst-docutils-latex = "myst_parser.docutils_:cli_latex" -myst-docutils-xml = "myst_parser.docutils_:cli_xml" -myst-docutils-pseudoxml = "myst_parser.docutils_:cli_pseudoxml" +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" @@ -105,3 +105,6 @@ warn_unused_ignores = true [[tool.mypy.overrides]] module = ["docutils.*", "yaml.*"] ignore_missing_imports = true + +[tool.coverage.run] +omit = ["*/_docs.py"] diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 11b20c47..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Top-level configuration for pytest.""" -try: - import sphinx # noqa: F401 -except ImportError: - pass -else: - # only use when Sphinx is installed, to allow testing myst-docutils - pytest_plugins = "sphinx.testing.fixtures" diff --git a/tests/test_commonmark/test_commonmark.py b/tests/test_commonmark/test_commonmark.py index 63e0c89a..3cee1eb8 100644 --- a/tests/test_commonmark/test_commonmark.py +++ b/tests/test_commonmark/test_commonmark.py @@ -5,8 +5,10 @@ import os import pytest +from markdown_it.renderer import RendererHTML -from myst_parser.main import to_html +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" @@ -27,7 +29,8 @@ def test_commonmark(entry): "Thematic breaks on the first line conflict with front matter syntax" ) test_case = entry["markdown"] - output = to_html(test_case) + md = create_md_parser(MdParserConfig(), RendererHTML) + output = md.render(test_case) if entry["example"] == 593: # this doesn't have any bearing on the output diff --git a/tests/test_docutils.py b/tests/test_docutils.py index b206958c..f6f5d36f 100644 --- a/tests/test_docutils.py +++ b/tests/test_docutils.py @@ -6,7 +6,8 @@ from docutils import VersionInfo, __version_info__ from typing_extensions import Literal -from myst_parser.docutils_ import ( +from myst_parser.mdit_to_docutils.base import make_document +from myst_parser.parsers.docutils_ import ( Parser, attr_to_optparse_option, cli_html, @@ -15,7 +16,6 @@ cli_pseudoxml, cli_xml, ) -from myst_parser.docutils_renderer import make_document def test_attr_to_optparse_option(): diff --git a/tests/test_html/test_html_to_nodes.py b/tests/test_html/test_html_to_nodes.py index 9774a32f..207a6274 100644 --- a/tests/test_html/test_html_to_nodes.py +++ b/tests/test_html/test_html_to_nodes.py @@ -4,8 +4,8 @@ import pytest from docutils import nodes -from myst_parser.html_to_nodes import html_to_nodes -from myst_parser.main import MdParserConfig +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 diff --git a/tests/test_html/test_parse_html.py b/tests/test_html/test_parse_html.py index 5b765887..3b4cdc1a 100644 --- a/tests/test_html/test_parse_html.py +++ b/tests/test_html/test_parse_html.py @@ -2,7 +2,7 @@ import pytest -from myst_parser.parse_html import tokenize_html +from myst_parser.parsers.parse_html import tokenize_html FIXTURE_PATH = Path(__file__).parent diff --git a/tests/test_renderers/fixtures/amsmath.md b/tests/test_renderers/fixtures/amsmath.md index 861e6256..fe3713ea 100644 --- a/tests/test_renderers/fixtures/amsmath.md +++ b/tests/test_renderers/fixtures/amsmath.md @@ -1,55 +1,51 @@ --------------------------------- Single Line: . \begin{equation} a \end{equation} . - + - + \begin{equation} a \end{equation} . --------------------------------- Multi Line: . \begin{equation} a \end{equation} . - + - + \begin{equation} a \end{equation} . --------------------------------- Multi Line no number: . \begin{equation*} a \end{equation*} . - + \begin{equation*} a \end{equation*} . --------------------------------- In list: . - \begin{equation} a = 1 \end{equation} . - + - + \begin{equation} a = 1 \end{equation} diff --git a/tests/test_renderers/fixtures/containers.md b/tests/test_renderers/fixtures/containers.md index 89bb699b..f67bea84 100644 --- a/tests/test_renderers/fixtures/containers.md +++ b/tests/test_renderers/fixtures/containers.md @@ -1,18 +1,16 @@ --------------------------------- Basic note: . ::: {note} *hallo* ::: . - + hallo . --------------------------------- Admonition with options: . ::: {admonition} A **title** @@ -21,10 +19,10 @@ Admonition with options: *hallo* ::: . - + - A + A <strong> title <paragraph> diff --git a/tests/test_renderers/fixtures/definition_lists.md b/tests/test_renderers/fixtures/definition_lists.md index 33529228..c064fa2c 100644 --- a/tests/test_renderers/fixtures/definition_lists.md +++ b/tests/test_renderers/fixtures/definition_lists.md @@ -1,4 +1,3 @@ --------------------------- Simple: . Term **1** @@ -18,7 +17,7 @@ Term 3 : other . -<document source="notset"> +<document source="<src>/index.md"> <definition_list classes="simple myst"> <definition_list_item> <term> diff --git a/tests/test_renderers/fixtures/directive_options.md b/tests/test_renderers/fixtures/directive_options.md index 838a1091..b9ae64a8 100644 --- a/tests/test_renderers/fixtures/directive_options.md +++ b/tests/test_renderers/fixtures/directive_options.md @@ -3,82 +3,76 @@ Test Directive 1: ```{restructuredtext-test-directive} ``` . -<document source="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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} @@ -86,15 +80,14 @@ Test Directive 7: foo ``` . -<document source="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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} @@ -105,15 +98,14 @@ option2: b foo ``` . -<document source="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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} @@ -122,15 +114,14 @@ Test Directive Options 2: foo ``` . -<document source="notset"> - <system_message level="1" line="1" source="notset" type="INFO"> +<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} @@ -139,8 +130,8 @@ Test Directive Options Error: foo ``` . -<document source="notset"> - <system_message level="3" line="1" source="notset" type="ERROR"> +<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: @@ -152,18 +143,16 @@ foo foo . - ------------------------------ Unknown Directive: . ```{unknown} ``` . -<document source="notset"> - <system_message level="3" line="1" source="notset" type="ERROR"> +<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="notset" type="INFO"> + <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. diff --git a/tests/test_renderers/fixtures/docutil_syntax_elements.md b/tests/test_renderers/fixtures/docutil_syntax_elements.md index 93718030..9b59f3a1 100644 --- a/tests/test_renderers/fixtures/docutil_syntax_elements.md +++ b/tests/test_renderers/fixtures/docutil_syntax_elements.md @@ -1,4 +1,3 @@ ---------------------------- Raw . foo @@ -8,7 +7,6 @@ foo foo . ---------------------------- Hard-break . foo\ @@ -24,7 +22,6 @@ bar bar . ---------------------------- Strong: . **foo** @@ -35,7 +32,6 @@ Strong: foo . ---------------------------- Emphasis . *foo* @@ -46,7 +42,6 @@ Emphasis foo . ---------------------------- Escaped Emphasis: . \*foo* @@ -56,7 +51,6 @@ Escaped Emphasis: *foo* . --------------------------- Mixed Inline . a *b* **c** `abc` \\* @@ -75,7 +69,6 @@ a *b* **c** `abc` \\* \* . --------------------------- Inline Code: . `foo` @@ -86,7 +79,6 @@ Inline Code: foo . --------------------------- Heading: . # foo @@ -97,7 +89,6 @@ Heading: foo . --------------------------- Heading Levels: . # a @@ -120,8 +111,6 @@ Heading Levels: d . - --------------------------- Block Code: . foo @@ -131,7 +120,6 @@ Block Code: foo . --------------------------- Fenced Code: . ```sh @@ -143,7 +131,6 @@ foo foo . --------------------------- Fenced Code no language: . ``` @@ -155,7 +142,6 @@ foo foo . --------------------------- Fenced Code no language with trailing whitespace: . ``` @@ -167,7 +153,6 @@ foo foo . --------------------------- Image empty: . ![]() @@ -177,7 +162,6 @@ Image empty: <image alt="" uri=""> . --------------------------- Image with alt and title: . ![alt](src "title") @@ -187,7 +171,6 @@ Image with alt and title: <image alt="alt" title="title" uri="src"> . --------------------------- Image with escapable html: . ![alt](http://www.google<>.com) @@ -197,7 +180,6 @@ Image with escapable html: <image alt="alt" uri="http://www.google%3C%3E.com"> . --------------------------- Block Quote: . > *foo* @@ -209,7 +191,6 @@ Block Quote: foo . --------------------------- Bullet List: . - *foo* @@ -227,7 +208,6 @@ Bullet List: bar . --------------------------- Nested Bullets . - a @@ -253,7 +233,6 @@ Nested Bullets d . --------------------------- Enumerated List: . 1. *foo* @@ -286,7 +265,6 @@ para enumerator . --------------------------- Nested Enumrated List: . 1. a @@ -307,7 +285,6 @@ Nested Enumrated List: c . --------------------------- Sphinx Role containing backtick: . {code}``a=1{`}`` @@ -318,7 +295,6 @@ Sphinx Role containing backtick: a=1{`} . --------------------------- Target: . (target)= @@ -327,7 +303,6 @@ Target: <target ids="target" names="target"> . --------------------------- Target with whitespace: . (target with space)= @@ -336,7 +311,6 @@ Target with whitespace: <target ids="target-with-space" names="target\ with\ space"> . --------------------------- Referencing: . (target)= @@ -370,7 +344,6 @@ Title alt3 . --------------------------- Comments: . line 1 @@ -386,7 +359,6 @@ line 2 line 2 . --------------------------- Block Break: . +++ string @@ -396,7 +368,6 @@ Block Break: string . --------------------------- Link Reference: . [name][key] @@ -409,7 +380,6 @@ Link Reference: name . --------------------------- Link Reference short version: . [name] @@ -422,7 +392,6 @@ Link Reference short version: name . --------------------------- Block Quotes: . ```{epigraph} @@ -443,7 +412,6 @@ a b*c* b . --------------------------- Link Definition in directive: . ```{note} @@ -459,7 +427,6 @@ Link Definition in directive: a . --------------------------- Link Definition in nested directives: . ```{note} @@ -486,7 +453,6 @@ Link Definition in nested directives: <note> . --------------------------- Footnotes: . [^a] @@ -504,7 +470,6 @@ Footnotes: text . --------------------------- Footnotes nested blocks: . [^a] @@ -548,7 +513,6 @@ finish c . --------------------------- Front Matter: . --- @@ -583,7 +547,6 @@ c: {"d": 2} . --------------------------- Front Matter Biblio: . --- @@ -702,7 +665,6 @@ other: Something else Something else . --------------------------- Front Matter Bad Yaml: . --- @@ -710,26 +672,20 @@ a: { --- . <document source="notset"> - <system_message level="3" line="1" source="notset" type="ERROR"> + <system_message level="2" line="1" source="notset" type="WARNING"> <paragraph> - Front matter block: - while parsing a flow node - expected the node content, but found '<stream end>' - in "<unicode string>", line 1, column 5: - a: { - ^ - <literal_block xml:space="preserve"> - a: { + Malformed YAML [myst.topmatter] . Front Matter HTML Meta . --- -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 +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"> @@ -767,7 +723,6 @@ html_meta: <meta content="text/html; charset=ISO-8859-1" http-equiv="Content-Type"> . --------------------------- Full Test: . --- diff --git a/tests/test_renderers/fixtures/dollarmath.md b/tests/test_renderers/fixtures/dollarmath.md index d3acabcb..e3e5fd23 100644 --- a/tests/test_renderers/fixtures/dollarmath.md +++ b/tests/test_renderers/fixtures/dollarmath.md @@ -1,21 +1,19 @@ --------------------------- Inline Math: . $foo$ . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <math> foo . --------------------------- Inline Math, multi-line: . a $foo bar$ b . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> a <math> @@ -24,42 +22,38 @@ bar$ b b . --------------------------- Inline Math, multi-line with line break (invalid): . a $foo bar$ b . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> a $foo <paragraph> bar$ b . --------------------------- Math Block: . $$foo$$ . -<document source="notset"> +<document source="<src>/index.md"> <math_block nowrap="False" number="True" xml:space="preserve"> foo . --------------------------- Math Block With Equation Label: . $$foo$$ (abc) . -<document source="notset"> +<document source="<src>/index.md"> <target ids="equation-abc"> - <math_block docname="mock_docname" label="abc" nowrap="False" number="1" xml:space="preserve"> + <math_block docname="index" label="abc" nowrap="False" number="1" xml:space="preserve"> foo . --------------------------- Math Block multiple: . $$ @@ -70,12 +64,12 @@ $$ b = 2 $$ (a) . -<document source="notset"> +<document source="<src>/index.md"> <math_block nowrap="False" number="True" xml:space="preserve"> a = 1 <target ids="equation-a"> - <math_block docname="mock_docname" label="a" nowrap="False" number="1" xml:space="preserve"> + <math_block docname="index" label="a" nowrap="False" number="1" xml:space="preserve"> b = 2 . diff --git a/tests/test_renderers/fixtures/eval_rst.md b/tests/test_renderers/fixtures/eval_rst.md index 421aeb01..9f21bd19 100644 --- a/tests/test_renderers/fixtures/eval_rst.md +++ b/tests/test_renderers/fixtures/eval_rst.md @@ -4,7 +4,7 @@ eval-rst link `MyST Parser <https://myst-parser.readthedocs.io/>`_ ``` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <reference name="MyST Parser" refuri="https://myst-parser.readthedocs.io/"> MyST Parser @@ -16,7 +16,7 @@ eval-rst bold ```{eval-rst} **bold** . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <strong> bold diff --git a/tests/test_renderers/fixtures/reporter_warnings.md b/tests/test_renderers/fixtures/reporter_warnings.md index 22b53329..e9998b90 100644 --- a/tests/test_renderers/fixtures/reporter_warnings.md +++ b/tests/test_renderers/fixtures/reporter_warnings.md @@ -37,24 +37,43 @@ Bad Front Matter: a: { --- . -<string>:1: (ERROR/3) Front matter block: -while parsing a flow node -expected the node content, but found '<stream end>' - in "<unicode string>", line 1, column 5: - 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 . --- -html_meta: - empty: - name noequals: value +myst: + html_meta: + name noequals: value --- . -<string>:: (ERROR/3) Error parsing meta tag attribute "empty": No content. <string>:: (ERROR/3) Error parsing meta tag attribute "name noequals": no '=' in noequals. . diff --git a/tests/test_renderers/fixtures/sphinx_directives.md b/tests/test_renderers/fixtures/sphinx_directives.md index 72107736..16a5fa74 100644 --- a/tests/test_renderers/fixtures/sphinx_directives.md +++ b/tests/test_renderers/fixtures/sphinx_directives.md @@ -1,28 +1,25 @@ --------------------------------- default-role (`sphinx.directives.DefaultRole`): . ```{default-role} ``` . -<document source="notset"> +<document source="<src>/index.md"> . --------------------------------- default-domain (`sphinx.directives.DefaultDomain`): . ```{default-domain} mydomain ``` . -<document source="notset"> +<document source="<src>/index.md"> . --------------------------------- SPHINX4 object (`sphinx.directives.ObjectDescription`): . ```{object} something ``` . -<document source="notset"> +<document source="<src>/index.md"> <index entries=""> <desc classes="object" desctype="object" domain="" noindex="False" objtype="object"> <desc_signature classes="sig sig-object"> @@ -31,17 +28,15 @@ SPHINX4 object (`sphinx.directives.ObjectDescription`): <desc_content> . --------------------------------- highlight (`sphinx.directives.code.Highlight`): . ```{highlight} something ``` . -<document source="notset"> +<document source="<src>/index.md"> <highlightlang force="False" lang="something" linenothreshold="9223372036854775807"> . --------------------------------- code-block (`sphinx.directives.code.CodeBlock`): . ```{code-block} @@ -50,23 +45,20 @@ code-block (`sphinx.directives.code.CodeBlock`): a=1 ``` . -<document source="notset"> +<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="notset"> +<document source="<src>/index.md"> <literal_block force="False" highlight_args="{}" language="default" xml:space="preserve"> . --------------------------------- -literalinclude (`sphinx.directives.code.LiteralInclude`): SKIP: Tested in sphinx builds . ```{literalinclude} /path/to/file @@ -78,56 +70,50 @@ SKIP: Tested in sphinx builds Include file '/srcdir/path/to/file' not found or reading it failed . --------------------------------- toctree (`sphinx.directives.other.TocTree`): . ```{toctree} ``` . -<document source="notset"> +<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="mock_docname" titlesonly="False"> + <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="notset"> +<document source="<src>/index.md"> . --------------------------------- moduleauthor (`sphinx.directives.other.Author`): . ```{moduleauthor} ringo starr ``` . -<document source="notset"> +<document source="<src>/index.md"> . --------------------------------- codeauthor (`sphinx.directives.other.Author`): . ```{codeauthor} paul mcartney ``` . -<document source="notset"> +<document source="<src>/index.md"> . --------------------------------- index (`sphinx.directives.other.Index`): . ```{index} something ``` . -<document source="notset"> +<document source="<src>/index.md"> <index entries="('single',\ 'something',\ 'index-0',\ '',\ None)" inline="False"> <target ids="index-0"> . --------------------------------- seealso (`sphinx.directives.other.SeeAlso`): . ```{seealso} @@ -135,34 +121,31 @@ seealso (`sphinx.directives.other.SeeAlso`): a ``` . -<document source="notset"> +<document source="<src>/index.md"> <seealso> <paragraph> a . --------------------------------- tabularcolumns (`sphinx.directives.other.TabularColumns`): . ```{tabularcolumns} spec ``` . -<document source="notset"> +<document source="<src>/index.md"> <tabular_col_spec spec="spec"> . --------------------------------- centered (`sphinx.directives.other.Centered`): . ```{centered} text ``` . -<document source="notset"> +<document source="<src>/index.md"> <centered> text . --------------------------------- acks (`sphinx.directives.other.Acks`): . ```{acks} @@ -170,7 +153,7 @@ acks (`sphinx.directives.other.Acks`): - name ``` . -<document source="notset"> +<document source="<src>/index.md"> <acks> <bullet_list bullet="-"> <list_item> @@ -178,7 +161,6 @@ acks (`sphinx.directives.other.Acks`): name . --------------------------------- SPHINX4 hlist (`sphinx.directives.other.HList`): . ```{hlist} @@ -186,7 +168,7 @@ SPHINX4 hlist (`sphinx.directives.other.HList`): - item ``` . -<document source="notset"> +<document source="<src>/index.md"> <hlist ncolumns="2"> <hlistcol> <bullet_list> @@ -197,18 +179,15 @@ SPHINX4 hlist (`sphinx.directives.other.HList`): <bullet_list> . --------------------------------- only (`sphinx.directives.other.Only`): . ```{only} expr ``` . -<document source="notset"> +<document source="<src>/index.md"> <only expr="expr"> . --------------------------------- -include (`sphinx.directives.other.Include`): SKIP: Tested in sphinx builds . ```{include} path/to/include @@ -217,7 +196,6 @@ SKIP: Tested in sphinx builds <document source="notset"> . --------------------------------- figure (`sphinx.directives.patches.Figure`): . ```{figure} path/to/figure @@ -227,7 +205,7 @@ figure (`sphinx.directives.patches.Figure`): legend ``` . -<document source="notset"> +<document source="<src>/index.md"> <figure> <image uri="path/to/figure"> <caption> @@ -238,8 +216,6 @@ legend legend . --------------------------------- -meta (`sphinx.directives.patches.Meta`): SKIP: MockingError: MockState has not yet implemented attribute 'nested_list_parse' . ```{meta} @@ -249,7 +225,6 @@ foo <document source="notset"> . --------------------------------- table (`sphinx.directives.patches.RSTTable`): . ```{table} *title* @@ -260,7 +235,7 @@ table (`sphinx.directives.patches.RSTTable`): | 1 | 2 | ``` . -<document source="notset"> +<document source="<src>/index.md"> <table classes="colwidths-auto" ids="name" names="name"> <title> <emphasis> @@ -286,7 +261,6 @@ table (`sphinx.directives.patches.RSTTable`): 2 . --------------------------------- csv-table (`sphinx.directives.patches.CSVTable`): . ```{csv-table} @@ -294,7 +268,7 @@ csv-table (`sphinx.directives.patches.CSVTable`): "Albatross", 2.99, "On a stick!" ``` . -<document source="notset"> +<document source="<src>/index.md"> <table> <tgroup cols="3"> <colspec colwidth="33"> @@ -313,7 +287,6 @@ csv-table (`sphinx.directives.patches.CSVTable`): On a stick! . --------------------------------- list-table (`sphinx.directives.patches.ListTable`): . ```{list-table} @@ -321,7 +294,7 @@ list-table (`sphinx.directives.patches.ListTable`): * - item ``` . -<document source="notset"> +<document source="<src>/index.md"> <table> <tgroup cols="1"> <colspec colwidth="100"> @@ -332,7 +305,6 @@ list-table (`sphinx.directives.patches.ListTable`): item . --------------------------------- code (`sphinx.directives.patches.Code`): . ```{code} python @@ -340,60 +312,56 @@ code (`sphinx.directives.patches.Code`): a ``` . -<document source="notset"> +<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="notset"> - <math_block docname="mock_docname" label="True" nowrap="False" number="True" xml:space="preserve"> +<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="notset"> +<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="notset"> +<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="notset"> +<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} @@ -403,7 +371,7 @@ term 2 : B Definition of both terms. ``` . -<document source="notset"> +<document source="<src>/index.md"> <glossary> <definition_list classes="glossary"> <definition_list_item> @@ -418,25 +386,23 @@ term 2 : B Definition of both terms. . --------------------------------- SPHINX3 productionlist (`sphinx.domains.std.ProductionList`): . ```{productionlist} try_stmt: try1_stmt | try2_stmt ``` . -<document source="notset"> +<document source="<src>/index.md"> <productionlist> <production ids="grammar-token-try_stmt grammar-token-try-stmt" tokenname="try_stmt" xml:space="preserve"> try1_stmt | try2_stmt . --------------------------------- SPHINX4 cmdoption (`sphinx.domains.std.Cmdoption`): . ```{cmdoption} a ``` . -<document source="notset"> +<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"> @@ -446,13 +412,12 @@ SPHINX4 cmdoption (`sphinx.domains.std.Cmdoption`): <desc_content> . --------------------------------- SPHINX4 rst:directive (`sphinx.domains.rst.ReSTDirective`): . ```{rst:directive} a ``` . -<document source="notset"> +<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"> @@ -461,13 +426,12 @@ SPHINX4 rst:directive (`sphinx.domains.rst.ReSTDirective`): <desc_content> . --------------------------------- SPHINX4 rst:directive:option (`sphinx.domains.rst.ReSTDirectiveOption`): . ```{rst:directive:option} a ``` . -<document source="notset"> +<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 directive:option--a"> diff --git a/tests/test_renderers/fixtures/sphinx_roles.md b/tests/test_renderers/fixtures/sphinx_roles.md index ee9c74c9..37683dbe 100644 --- a/tests/test_renderers/fixtures/sphinx_roles.md +++ b/tests/test_renderers/fixtures/sphinx_roles.md @@ -1,196 +1,179 @@ --------------------------------- c:func (`sphinx.domains.c.CXRefRole`): . {c:func}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="c" refexplicit="False" reftarget="a" reftype="func" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="c" refexplicit="False" reftarget="a" reftype="member" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="c" refexplicit="False" reftarget="a" reftype="macro" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="c" refexplicit="False" reftarget="a" reftype="data" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="c" refexplicit="False" reftarget="a" reftype="type" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="any" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="class" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="struct" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="union" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="func" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="member" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="var" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="type" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="concept" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="enum" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="cpp" refexplicit="False" reftarget="a" reftype="enumerator" refwarn="False"> + <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` @@ -203,7 +186,6 @@ SKIP cpp:expr (`sphinx.domains.cpp.CPPExprRole`): a . --------------------------------- SKIP cpp:texpr (`sphinx.domains.cpp.CPPExprRole`): . {cpp:texpr}`a` @@ -216,369 +198,337 @@ SKIP cpp:texpr (`sphinx.domains.cpp.CPPExprRole`): a . --------------------------------- js:func (`sphinx.domains.javascript.JSXRefRole`): . {js:func}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref js:module="True" js:object="True" refdoc="mock_docname" refdomain="js" refexplicit="False" reftarget="a" reftype="func" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref js:module="True" js:object="True" refdoc="mock_docname" refdomain="js" refexplicit="False" reftarget="a" reftype="meth" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref js:module="True" js:object="True" refdoc="mock_docname" refdomain="js" refexplicit="False" reftarget="a" reftype="class" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref js:module="True" js:object="True" refdoc="mock_docname" refdomain="js" refexplicit="False" reftarget="a" reftype="data" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref js:module="True" js:object="True" refdoc="mock_docname" refdomain="js" refexplicit="False" reftarget="a" reftype="attr" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref js:module="True" js:object="True" refdoc="mock_docname" refdomain="js" refexplicit="False" reftarget="a" reftype="mod" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="math" refexplicit="False" reftarget="a" reftype="eq" refwarn="True"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="math" refexplicit="False" reftarget="a" reftype="numref" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="data" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="exc" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="func" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="class" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="const" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="attr" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="meth" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="mod" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref py:class="True" py:module="True" refdoc="mock_docname" refdomain="py" refexplicit="False" reftarget="a" reftype="obj" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="rst" refexplicit="False" reftarget="a" reftype="role" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> <literal_strong classes="program"> a . --------------------------------- option (`sphinx.domains.std.Cmdoption`): . {option}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="option" refwarn="True" std:program="True"> + <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="notset"> +<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="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="envvar" refwarn="False"> + <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="notset"> +<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="notset"> +<document source="<src>/index.md"> <paragraph> - <download_reference refdoc="mock_docname" refdomain="" refexplicit="False" reftarget="a" reftype="download" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="" refexplicit="True" reftarget="alt text" reftype="any" refwarn="True"> + <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="notset"> +<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="http://www.python.org/dev/peps/pep-0001/"> + <reference classes="pep" internal="False" refuri="https://peps.python.org/pep-0001/"> <strong> PEP 1 . --------------------------------- rfc (`sphinx.roles.RFC`): . {rfc}`1` . -<document source="notset"> +<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="http://tools.ietf.org/html/rfc1.html"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> <inline classes="guilabel" rawtext=":guilabel:`a`"> a . --------------------------------- menuselection (`sphinx.roles.MenuSelection`): . {menuselection}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <inline classes="menuselection" rawtext=":menuselection:`a`"> a . --------------------------------- file (`sphinx.roles.EmphasizedLiteral`): . {file}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <literal classes="file" role="file"> a . --------------------------------- samp (`sphinx.roles.EmphasizedLiteral`): . {samp}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <literal classes="samp" role="samp"> a . --------------------------------- -abbr (`sphinx.roles.Abbreviation`): SKIP: Non-deterministic output . {abbr}`a` @@ -589,55 +539,50 @@ SKIP: Non-deterministic output a . --------------------------------- rst:dir (`sphinx.roles.XRefRole`): . {rst:dir}`a` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="rst" refexplicit="False" reftarget="a" reftype="dir" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="token" refwarn="False"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="term" refwarn="True"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="ref" refwarn="True"> + <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 @@ -646,45 +591,42 @@ text a custom reference>` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="True" reftarget="and a custom reference" reftype="ref" refwarn="True"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="numref" refwarn="True"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="False" reftarget="a" reftype="keyword" refwarn="True"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="std" refexplicit="True" reftarget="heavy_tails" reftype="doc" refwarn="True"> + <pending_xref refdoc="index" refdomain="std" refexplicit="True" reftarget="heavy_tails" reftype="doc" refwarn="True"> <inline classes="xref std std-doc"> this lecture . diff --git a/tests/test_renderers/fixtures/sphinx_syntax_elements.md b/tests/test_renderers/fixtures/sphinx_syntax_elements.md index d706780c..1ac085c2 100644 --- a/tests/test_renderers/fixtures/sphinx_syntax_elements.md +++ b/tests/test_renderers/fixtures/sphinx_syntax_elements.md @@ -1,20 +1,18 @@ ---------------------------- Raw . foo . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> foo . ---------------------------- Hard-break . foo\ bar . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> foo <raw format="html" xml:space="preserve"> @@ -24,44 +22,40 @@ bar bar . ---------------------------- Strong: . **foo** . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <strong> foo . ---------------------------- Emphasis . *foo* . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <emphasis> foo . ---------------------------- Escaped Emphasis: . \*foo* . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> *foo* . --------------------------- Mixed Inline . a *b* **c** `abc` \\* . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> a <emphasis> @@ -75,29 +69,26 @@ a *b* **c** `abc` \\* \* . --------------------------- Inline Code: . `foo` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <literal> foo . --------------------------- Heading: . # foo . -<document source="notset"> +<document source="<src>/index.md"> <section ids="foo" names="foo"> <title> foo . --------------------------- Heading Levels: . # a @@ -105,7 +96,7 @@ Heading Levels: ### c # d . -<document source="notset"> +<document source="<src>/index.md"> <section ids="a" names="a"> <title> a @@ -120,102 +111,92 @@ Heading Levels: d . - --------------------------- Block Code: . foo . -<document source="notset"> +<document source="<src>/index.md"> <literal_block language="none" xml:space="preserve"> foo . --------------------------- Fenced Code: . ```sh foo ``` . -<document source="notset"> +<document source="<src>/index.md"> <literal_block language="sh" xml:space="preserve"> foo . --------------------------- Fenced Code no language: . ``` foo ``` . -<document source="notset"> +<document source="<src>/index.md"> <literal_block language="default" xml:space="preserve"> foo . --------------------------- Fenced Code no language with trailing whitespace: . ``` foo ``` . -<document source="notset"> +<document source="<src>/index.md"> <literal_block language="default" xml:space="preserve"> foo . --------------------------- Image empty: . ![]() . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <image alt="" uri=""> . --------------------------- Image with alt and title: . ![alt](src "title") . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <image alt="alt" title="title" uri="src"> . --------------------------- Image with escapable html: . ![alt](http://www.google<>.com) . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <image alt="alt" uri="http://www.google%3C%3E.com"> . --------------------------- Block Quote: . > *foo* . -<document source="notset"> +<document source="<src>/index.md"> <block_quote> <paragraph> <emphasis> foo . --------------------------- Bullet List: . - *foo* * bar . -<document source="notset"> +<document source="<src>/index.md"> <bullet_list bullet="-"> <list_item> <paragraph> @@ -227,7 +208,6 @@ Bullet List: bar . --------------------------- Nested Bullets . - a @@ -235,7 +215,7 @@ Nested Bullets - c - d . -<document source="notset"> +<document source="<src>/index.md"> <bullet_list bullet="-"> <list_item> <paragraph> @@ -253,7 +233,6 @@ Nested Bullets d . --------------------------- Enumerated List: . 1. *foo* @@ -265,7 +244,7 @@ para 10. starting 11. enumerator . -<document source="notset"> +<document source="<src>/index.md"> <enumerated_list enumtype="arabic" prefix="" suffix="."> <list_item> <paragraph> @@ -286,14 +265,13 @@ para enumerator . --------------------------- Nested Enumrated List: . 1. a 2. b 1. c . -<document source="notset"> +<document source="<src>/index.md"> <enumerated_list enumtype="arabic" prefix="" suffix="."> <list_item> <paragraph> @@ -307,36 +285,32 @@ Nested Enumrated List: c . --------------------------- Sphinx Role containing backtick: . {code}``a=1{`}`` . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <literal classes="code"> a=1{`} . --------------------------- Target: . (target)= . -<document source="notset"> +<document source="<src>/index.md"> <target ids="target" names="target"> . --------------------------- Target with whitespace: . (target with space)= . -<document source="notset"> +<document source="<src>/index.md"> <target ids="target-with-space" names="target\ with\ space"> . --------------------------- Referencing: . (target)= @@ -352,35 +326,34 @@ Title [alt3](#target3) . -<document source="notset"> +<document source="<src>/index.md"> <target ids="target" names="target"> <section ids="title" names="title"> <title> Title <paragraph> - <pending_xref refdoc="mock_docname" refdomain="True" refexplicit="True" reftarget="target" reftype="myst" refwarn="True"> + <pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="target" reftype="myst" refwarn="True"> <inline classes="xref myst"> alt1 <paragraph> - <pending_xref refdoc="mock_docname" refdomain="True" refexplicit="False" reftarget="target2" reftype="myst" refwarn="True"> + <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="mock_docname" refdomain="True" refexplicit="True" reftarget="#target3" reftype="myst" refwarn="True"> + <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="notset"> +<document source="<src>/index.md"> <paragraph> line 1 <comment xml:space="preserve"> @@ -389,43 +362,39 @@ line 2 line 2 . --------------------------- Block Break: . +++ string . -<document source="notset"> +<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="notset"> +<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="notset"> +<document source="<src>/index.md"> <paragraph> <reference refuri="https://www.google.com" title="a title"> name . --------------------------- Block Quotes: . ```{epigraph} @@ -434,7 +403,7 @@ a b*c* -- a**b** ``` . -<document source="notset"> +<document source="<src>/index.md"> <block_quote classes="epigraph"> <paragraph> a b @@ -446,7 +415,6 @@ a b*c* b . --------------------------- Link Definition in directive: . ```{note} @@ -455,15 +423,14 @@ Link Definition in directive: [a]: link . -<document source="notset"> +<document source="<src>/index.md"> <note> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="True" refexplicit="True" reftarget="link" reftype="myst" refwarn="True"> + <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} @@ -479,11 +446,11 @@ Link Definition in nested directives: [ref2]: link ``` . -<document source="notset"> +<document source="<src>/index.md"> <note> <note> <paragraph> - <pending_xref refdoc="mock_docname" refdomain="True" refexplicit="True" reftarget="link" reftype="myst" refwarn="True"> + <pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="link" reftype="myst" refwarn="True"> <inline classes="xref myst"> ref1 @@ -491,14 +458,13 @@ Link Definition in nested directives: <note> . --------------------------- Footnotes: . [^a] [^a]: footnote*text* . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <footnote_reference auto="1" ids="id1" refname="a"> <transition classes="footnotes"> @@ -509,7 +475,6 @@ Footnotes: text . --------------------------- Footnotes nested blocks: . [^a] @@ -527,7 +492,7 @@ xyz finish . -<document source="notset"> +<document source="<src>/index.md"> <paragraph> <footnote_reference auto="1" ids="id1" refname="a"> <paragraph> @@ -553,7 +518,6 @@ finish c . --------------------------- Front Matter: . --- @@ -563,7 +527,7 @@ c: d: 2 --- . -<document source="notset"> +<document source="<src>/index.md"> <field_list> <field> <field_name> @@ -588,7 +552,6 @@ c: {"d": 2} . --------------------------- Front Matter Biblio: . --- @@ -612,7 +575,7 @@ abstract: other: Something else --- . -<document source="notset"> +<document source="<src>/index.md"> <field_list> <field> <field_name> @@ -638,11 +601,11 @@ other: Something else <field_body> <paragraph> 1 Cedar Park Close - + Thundersley - + Essex - + <field> <field_name> contact @@ -685,16 +648,16 @@ other: Something else dedication <field_body> <paragraph> - To my + To my <emphasis> homies - + <field> <field_name> abstract <field_body> <paragraph> - Something something + Something something <strong> dark side @@ -707,37 +670,30 @@ other: Something else Something else . --------------------------- Front Matter Bad Yaml: . --- a: { --- . -<document source="notset"> - <system_message level="3" line="1" source="notset" type="ERROR"> +<document source="<src>/index.md"> + <system_message level="2" line="1" source="<src>/index.md" type="WARNING"> <paragraph> - Front matter block: - while parsing a flow node - expected the node content, but found '<stream end>' - in "<unicode string>", line 1, column 5: - a: { - ^ - <literal_block xml:space="preserve"> - a: { + Malformed YAML [myst.topmatter] . Front Matter HTML Meta . --- -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 +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"> +<document source="<src>/index.md"> <pending> .. internal attributes: .transform: docutils.transforms.components.Filter @@ -772,7 +728,6 @@ html_meta: <meta content="text/html; charset=ISO-8859-1" http-equiv="Content-Type"> . --------------------------- Full Test: . --- @@ -799,7 +754,7 @@ a = 1 [](target) . -<document source="notset"> +<document source="<src>/index.md"> <field_list> <field> <field_name> @@ -840,6 +795,6 @@ a = 1 <literal_block language="::python" xml:space="preserve"> a = 1 <paragraph> - <pending_xref refdoc="mock_docname" refdomain="True" refexplicit="False" reftarget="target" reftype="myst" refwarn="True"> + <pending_xref refdoc="index" refdomain="True" refexplicit="False" reftarget="target" reftype="myst" refwarn="True"> <inline classes="xref myst"> . diff --git a/tests/test_renderers/fixtures/tables.md b/tests/test_renderers/fixtures/tables.md index 65238d3a..f24a86cc 100644 --- a/tests/test_renderers/fixtures/tables.md +++ b/tests/test_renderers/fixtures/tables.md @@ -1,11 +1,10 @@ --------------------------- Simple: . a|b -|- 1|2 . -<document source="notset"> +<document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> <colspec colwidth="50.0"> @@ -28,13 +27,12 @@ a|b 2 . --------------------------- Header only: . | abc | def | | --- | --- | . -<document source="notset"> +<document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> <colspec colwidth="50.0"> @@ -49,14 +47,13 @@ Header only: def . --------------------------- Aligned: . a | b | c :-|:-:| -: 1 | 2 | 3 . -<document source="notset"> +<document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="3"> <colspec colwidth="33.33"> @@ -86,14 +83,13 @@ a | b | c 3 . --------------------------- Nested syntax: . | *a* | __*b*__ | | --- | -------- | |c | {sub}`x` | . -<document source="notset"> +<document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> <colspec colwidth="50.0"> @@ -120,14 +116,13 @@ Nested syntax: x . --------------------------- External links: . a|b |-|-| [link-a](https://www.google.com/)|[link-b](https://www.python.org/) . -<document source="notset"> +<document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> <colspec colwidth="50.0"> @@ -150,4 +145,4 @@ a|b <paragraph> <reference refuri="https://www.python.org/"> link-b -. \ No newline at end of file +. diff --git a/tests/test_renderers/test_error_reporting.py b/tests/test_renderers/test_error_reporting.py index 72327fdb..2547e85f 100644 --- a/tests/test_renderers/test_error_reporting.py +++ b/tests/test_renderers/test_error_reporting.py @@ -5,7 +5,7 @@ import pytest from docutils.core import publish_doctree -from myst_parser.docutils_ import Parser +from myst_parser.parsers.docutils_ import Parser FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures") diff --git a/tests/test_renderers/test_fixtures_docutils.py b/tests/test_renderers/test_fixtures_docutils.py index b1897b1e..d80792f3 100644 --- a/tests/test_renderers/test_fixtures_docutils.py +++ b/tests/test_renderers/test_fixtures_docutils.py @@ -9,43 +9,68 @@ import pytest from docutils.core import Publisher, publish_doctree -from myst_parser.docutils_ import Parser -from myst_parser.docutils_renderer import DocutilsRenderer, make_document -from myst_parser.main import MdParserConfig, create_md_parser +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): - parser = create_md_parser( - MdParserConfig(highlight_code_blocks=False), DocutilsRenderer +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}, ) - parser.options["document"] = document = make_document() - parser.render(file_params.content) + # in docutils 0.18 footnote ids have changed - outcome = document.pformat().replace('"footnote-reference-1"', '"id1"') + 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): - """Test output of docutils roles.""" - parser = create_md_parser(MdParserConfig(), DocutilsRenderer) - parser.options["document"] = document = make_document() - parser.render(file_params.content) - file_params.assert_expected(document.pformat(), rstrip_lines=True) +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(), + ) + + file_params.assert_expected(doctree.pformat(), rstrip_lines=True) @pytest.mark.param_file(FIXTURE_PATH / "docutil_directives.md") -def test_docutils_directives(file_params): +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) - parser = create_md_parser(MdParserConfig(), DocutilsRenderer) - parser.options["document"] = document = make_document() - parser.render(file_params.content) - file_params.assert_expected(document.pformat(), rstrip_lines=True) + + 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") diff --git a/tests/test_renderers/test_fixtures_sphinx.py b/tests/test_renderers/test_fixtures_sphinx.py index 11427806..abb57aa1 100644 --- a/tests/test_renderers/test_fixtures_sphinx.py +++ b/tests/test_renderers/test_fixtures_sphinx.py @@ -2,44 +2,44 @@ Note, the output AST is before any transforms are applied. """ +from __future__ import annotations + import re import sys from pathlib import Path import pytest import sphinx +from sphinx_pytest.plugin import CreateDoctree -from myst_parser.main import MdParserConfig, to_docutils -from myst_parser.sphinx_renderer import SphinxRenderer, mock_sphinx_env +from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures") -def test_minimal_sphinx(): - with mock_sphinx_env(conf={"author": "bob geldof"}, with_builder=True) as app: - assert app.config["author"] == "bob geldof" - - @pytest.mark.param_file(FIXTURE_PATH / "sphinx_syntax_elements.md") -def test_syntax_elements(file_params): - document = to_docutils(file_params.content, in_sphinx_env=True) - file_params.assert_expected(document.pformat(), rstrip_lines=True) +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): - document = to_docutils(file_params.content, in_sphinx_env=True) - file_params.assert_expected(document.pformat(), rstrip_lines=True) +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): - document = to_docutils(file_params.content) - file_params.assert_expected(document.pformat(), rstrip_lines=True) +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): +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"): @@ -48,77 +48,77 @@ def test_sphinx_directives(file_params): pytest.skip(file_params.title) elif file_params.title.startswith("SPHINX4") and sphinx.version_info[0] < 4: pytest.skip(file_params.title) - document = to_docutils(file_params.content, in_sphinx_env=True).pformat() + + 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 - document = document.replace('<glossary sorted="False">', "<glossary>") + pformat = pformat.replace('<glossary sorted="False">', "<glossary>") # see https://github.com/executablebooks/MyST-Parser/issues/522 if sys.maxsize == 2147483647: - document = document.replace('"2147483647"', '"9223372036854775807"') - file_params.assert_expected(document, rstrip_lines=True) + 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): +def test_sphinx_roles(file_params, sphinx_doctree_no_tr: CreateDoctree): if file_params.title.startswith("SKIP"): pytest.skip(file_params.title) elif file_params.title.startswith("SPHINX4") and sphinx.version_info[0] < 4: pytest.skip(file_params.title) - document = to_docutils(file_params.content, in_sphinx_env=True) - actual = document.pformat() + + 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 - actual = re.sub('cpp:parent_key="[^"]*"', 'cpp:parent_key=""', actual) + 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 - actual = actual.replace( + 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(actual, rstrip_lines=True) + file_params.assert_expected(pformat, rstrip_lines=True) @pytest.mark.param_file(FIXTURE_PATH / "dollarmath.md") -def test_dollarmath(file_params, monkeypatch): - document = to_docutils( - file_params.content, - MdParserConfig(enable_extensions=["dollarmath"]), - in_sphinx_env=True, +def test_dollarmath(file_params, sphinx_doctree_no_tr: CreateDoctree): + sphinx_doctree_no_tr.set_conf( + {"extensions": ["myst_parser"], "myst_enable_extensions": ["dollarmath"]} ) - file_params.assert_expected(document.pformat(), rstrip_lines=True) + 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, monkeypatch): +def test_amsmath(file_params, sphinx_doctree_no_tr: CreateDoctree, monkeypatch): monkeypatch.setattr(SphinxRenderer, "_random_label", lambda self: "mock-uuid") - document = to_docutils( - file_params.content, - MdParserConfig(enable_extensions=["amsmath"]), - in_sphinx_env=True, + sphinx_doctree_no_tr.set_conf( + {"extensions": ["myst_parser"], "myst_enable_extensions": ["amsmath"]} ) - file_params.assert_expected(document.pformat(), rstrip_lines=True) + 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, monkeypatch): +def test_containers(file_params, sphinx_doctree_no_tr: CreateDoctree, monkeypatch): monkeypatch.setattr(SphinxRenderer, "_random_label", lambda self: "mock-uuid") - document = to_docutils( - file_params.content, - MdParserConfig(enable_extensions=["colon_fence"]), - in_sphinx_env=True, + sphinx_doctree_no_tr.set_conf( + {"extensions": ["myst_parser"], "myst_enable_extensions": ["colon_fence"]} ) - file_params.assert_expected(document.pformat(), rstrip_lines=True) + 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): - document = to_docutils(file_params.content, in_sphinx_env=True) - file_params.assert_expected(document.pformat(), rstrip_lines=True) +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): - document = to_docutils( - file_params.content, - MdParserConfig(enable_extensions=["deflist"]), - in_sphinx_env=True, +def test_definition_lists(file_params, sphinx_doctree_no_tr: CreateDoctree): + sphinx_doctree_no_tr.set_conf( + {"extensions": ["myst_parser"], "myst_enable_extensions": ["deflist"]} ) - file_params.assert_expected(document.pformat(), rstrip_lines=True) + result = sphinx_doctree_no_tr(file_params.content, "index.md") + file_params.assert_expected(result.pformat("index"), rstrip_lines=True) diff --git a/tests/test_renderers/test_include_directive.py b/tests/test_renderers/test_include_directive.py index 20e1a9d6..f02b246f 100644 --- a/tests/test_renderers/test_include_directive.py +++ b/tests/test_renderers/test_include_directive.py @@ -1,44 +1,52 @@ import os +from io import StringIO from pathlib import Path import pytest +from docutils.core import publish_doctree -from myst_parser.docutils_renderer import make_document -from myst_parser.main import to_docutils +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): +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") - document = make_document(str(tmp_path / "test.md")) - to_docutils( - file_params.content, document=document, in_sphinx_env=True, srcdir=str(tmp_path) + + doctree = publish_doctree( + file_params.content, + parser=Parser(), + settings_overrides={"myst_highlight_code_blocks": False}, ) - output = document.pformat().replace(str(tmp_path) + os.sep, "tmpdir" + "/").rstrip() + + 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): +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`") - document = make_document(str(tmp_path / "test.md")) - messages = [] - - def observer(msg_node): - if msg_node["level"] > 1: - messages.append( - msg_node.astext().replace(str(tmp_path) + os.sep, "tmpdir" + "/") - ) - - document.reporter.attach_observer(observer) - document.reporter.halt_level = 6 - to_docutils( - file_params.content, document=document, in_sphinx_env=True, srcdir=str(tmp_path) + + 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, ) - file_params.assert_expected("\n".join(messages), rstrip=True) diff --git a/tests/test_renderers/test_myst_config.py b/tests/test_renderers/test_myst_config.py index fee43de3..0f58cd76 100644 --- a/tests/test_renderers/test_myst_config.py +++ b/tests/test_renderers/test_myst_config.py @@ -6,7 +6,7 @@ import pytest from docutils.core import Publisher, publish_doctree -from myst_parser.docutils_ import Parser +from myst_parser.parsers.docutils_ import Parser FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures") diff --git a/tests/test_renderers/test_myst_refs.py b/tests/test_renderers/test_myst_refs.py index c56b256c..083b34a2 100644 --- a/tests/test_renderers/test_myst_refs.py +++ b/tests/test_renderers/test_myst_refs.py @@ -1,12 +1,5 @@ -import os - import pytest -from sphinx.application import Sphinx -from sphinx.errors import SphinxWarning - -from myst_parser.main import MdParserConfig -from myst_parser.sphinx_parser import parse -from myst_parser.sphinx_renderer import mock_sphinx_env +from sphinx_pytest.plugin import CreateDoctree @pytest.mark.parametrize( @@ -23,24 +16,23 @@ ("ref_colon", "(ref:colon)=\n# Title\n[](ref:colon)", False), ], ) -def test_parse(test_name, text, should_warn, file_regression): +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 - with mock_sphinx_env( - conf={"extensions": ["myst_parser"]}, - srcdir="root", - with_builder=True, - raise_on_warning=True, - ) as app: # type: Sphinx - app.env.myst_config = MdParserConfig() - document = parse(app, text, docname="index") - if should_warn: - with pytest.raises(SphinxWarning): - app.env.apply_post_transforms(document, "index") - else: - app.env.apply_post_transforms(document, "index") + doctree = result.get_resolved_doctree("index") - content = document.pformat() - # windows fix - content = content.replace("root" + os.sep + "index.md", "root/index.md") + if should_warn: + assert result.warnings + else: + assert not result.warnings - file_regression.check(content, basename=test_name, extension=".xml") + doctree["source"] = "root/index.md" + file_regression.check(doctree.pformat(), basename=test_name, extension=".xml") diff --git a/tests/test_renderers/test_myst_refs/duplicate.xml b/tests/test_renderers/test_myst_refs/duplicate.xml index 35d4e8df..755906c8 100644 --- a/tests/test_renderers/test_myst_refs/duplicate.xml +++ b/tests/test_renderers/test_myst_refs/duplicate.xml @@ -1,7 +1,9 @@ -<document ids="title index" names="title index" source="root/index.md" title="Title"> - <title> - Title +<document source="root/index.md"> <target refid="index"> - <paragraph> - <pending_xref refdoc="index" refdomain="True" refexplicit="False" reftarget="index" reftype="myst" refwarn="True"> - <inline classes="xref myst"> + <section ids="title index" names="title index"> + <title> + Title + <paragraph> + <reference internal="True" refid="index"> + <inline classes="std std-ref"> + Title diff --git a/tests/test_renderers/test_myst_refs/missing.xml b/tests/test_renderers/test_myst_refs/missing.xml index 6c4b5be3..6bc72ade 100644 --- a/tests/test_renderers/test_myst_refs/missing.xml +++ b/tests/test_renderers/test_myst_refs/missing.xml @@ -1,4 +1,3 @@ <document source="root/index.md"> <paragraph> - <pending_xref refdoc="index" refdomain="" refexplicit="False" reftarget="ref" reftype="myst" refwarn="True"> - <inline classes="xref myst"> + <inline classes="xref myst"> diff --git a/tests/test_renderers/test_myst_refs/ref.xml b/tests/test_renderers/test_myst_refs/ref.xml index 78e2a49e..e4ae200d 100644 --- a/tests/test_renderers/test_myst_refs/ref.xml +++ b/tests/test_renderers/test_myst_refs/ref.xml @@ -1,8 +1,9 @@ -<document ids="title ref" names="title ref" source="root/index.md" title="Title"> - <title> - Title +<document source="root/index.md"> <target refid="ref"> - <paragraph> - <reference internal="True" refid="ref"> - <inline classes="std std-ref"> - Title + <section ids="title ref" names="title ref"> + <title> + Title + <paragraph> + <reference internal="True" refid="ref"> + <inline classes="std std-ref"> + Title diff --git a/tests/test_renderers/test_myst_refs/ref_colon.xml b/tests/test_renderers/test_myst_refs/ref_colon.xml index affccd4a..f1e9923b 100644 --- a/tests/test_renderers/test_myst_refs/ref_colon.xml +++ b/tests/test_renderers/test_myst_refs/ref_colon.xml @@ -1,8 +1,9 @@ -<document ids="title ref-colon" names="title ref:colon" source="root/index.md" title="Title"> - <title> - Title +<document source="root/index.md"> <target refid="ref-colon"> - <paragraph> - <reference internal="True" refid="ref-colon"> - <inline classes="std std-ref"> - Title + <section ids="title ref-colon" names="title ref:colon"> + <title> + Title + <paragraph> + <reference internal="True" refid="ref-colon"> + <inline classes="std std-ref"> + Title diff --git a/tests/test_renderers/test_myst_refs/ref_nested.xml b/tests/test_renderers/test_myst_refs/ref_nested.xml index 34f99c83..be69ef22 100644 --- a/tests/test_renderers/test_myst_refs/ref_nested.xml +++ b/tests/test_renderers/test_myst_refs/ref_nested.xml @@ -1,9 +1,10 @@ -<document ids="title ref" names="title ref" source="root/index.md" title="Title"> - <title> - Title +<document source="root/index.md"> <target refid="ref"> - <paragraph> - <reference internal="True" refid="ref"> - <inline classes="std std-ref"> - <emphasis> - text + <section ids="title ref" names="title ref"> + <title> + Title + <paragraph> + <reference internal="True" refid="ref"> + <inline classes="std std-ref"> + <emphasis> + text diff --git a/tests/test_renderers/test_parse_directives.py b/tests/test_renderers/test_parse_directives.py index c85e6637..068a3d77 100644 --- a/tests/test_renderers/test_parse_directives.py +++ b/tests/test_renderers/test_parse_directives.py @@ -3,7 +3,7 @@ from docutils.parsers.rst.directives.admonitions import Note from docutils.parsers.rst.directives.body import Rubric -from myst_parser.parse_directives import DirectiveParsingError, parse_directive_text +from myst_parser.parsers.directives import DirectiveParsingError, parse_directive_text @pytest.mark.parametrize( diff --git a/tests/test_sphinx/conftest.py b/tests/test_sphinx/conftest.py index 7930b651..e7fb75a8 100644 --- a/tests/test_sphinx/conftest.py +++ b/tests/test_sphinx/conftest.py @@ -66,7 +66,7 @@ def read( outpath = path(os.path.join(str(app.srcdir), "_build", buildername, filename)) if not outpath.exists(): - raise IOError("no output file exists: {}".format(outpath)) + raise OSError(f"no output file exists: {outpath}") try: # introduced in sphinx 3.0 diff --git a/tests/test_sphinx/sourcedirs/substitutions/index.md b/tests/test_sphinx/sourcedirs/substitutions/index.md index 5c8aa727..e1dd672a 100644 --- a/tests/test_sphinx/sourcedirs/substitutions/index.md +++ b/tests/test_sphinx/sourcedirs/substitutions/index.md @@ -1,22 +1,23 @@ --- -substitutions: - text: "- text" - text_with_nest: > - output - with *Markdown* - {{ nested }} - nested: nested substitution - admonition: | - prefix - - ```{note} - A note {{ nested }} - ``` - inline_admonition: | - ```{note} - Inline note - ``` - override: Overridden by front matter +myst: + substitutions: + text: "- text" + text_with_nest: > + output + with *Markdown* + {{ nested }} + nested: nested substitution + admonition: | + prefix + + ```{note} + A note {{ nested }} + ``` + inline_admonition: | + ```{note} + Inline note + ``` + override: Overridden by front matter --- diff --git a/tests/test_sphinx/test_sphinx_builds.py b/tests/test_sphinx/test_sphinx_builds.py index e3025bcd..e8615a64 100644 --- a/tests/test_sphinx/test_sphinx_builds.py +++ b/tests/test_sphinx/test_sphinx_builds.py @@ -203,7 +203,7 @@ def test_extended_syntaxes( monkeypatch, ): """test setting addition configuration values.""" - from myst_parser.sphinx_renderer import SphinxRenderer + from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer monkeypatch.setattr(SphinxRenderer, "_random_label", lambda self: "mock-uuid") app.build() From 3c45b7e65506de5dcd643326207023f088f49748 Mon Sep 17 00:00:00 2001 From: Jean Abou-Samra <jean@abou-samra.fr> Date: Sun, 15 May 2022 12:48:12 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20floor=20table=20colum?= =?UTF-8?q?n=20widths=20to=20integers=20(#568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myst_parser/mdit_to_docutils/base.py | 2 +- .../fixtures/sphinx_directives.md | 4 ++-- tests/test_renderers/fixtures/tables.md | 22 +++++++++---------- .../test_sphinx/sourcedirs/texi_table/conf.py | 2 ++ .../sourcedirs/texi_table/index.md | 3 +++ tests/test_sphinx/test_sphinx_builds.py | 17 ++++++++++++++ .../test_basic.resolved.sphinx4.xml | 4 ++-- .../test_sphinx_builds/test_basic.sphinx4.xml | 4 ++-- .../test_gettext_html.resolved.sphinx4.xml | 2 +- .../test_gettext_html.sphinx4.xml | 2 +- 10 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 tests/test_sphinx/sourcedirs/texi_table/conf.py create mode 100644 tests/test_sphinx/sourcedirs/texi_table/index.md diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py index 099b5ad1..252cc353 100644 --- a/myst_parser/mdit_to_docutils/base.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -912,7 +912,7 @@ def render_table(self, token: SyntaxTreeNode) -> None: # column settings element maxcols = len(header_row.children) - colwidths = [round(100 / maxcols, 2)] * maxcols + colwidths = [100 // maxcols] * maxcols tgroup = nodes.tgroup(cols=len(colwidths)) table += tgroup for colwidth in colwidths: diff --git a/tests/test_renderers/fixtures/sphinx_directives.md b/tests/test_renderers/fixtures/sphinx_directives.md index 16a5fa74..2084558e 100644 --- a/tests/test_renderers/fixtures/sphinx_directives.md +++ b/tests/test_renderers/fixtures/sphinx_directives.md @@ -241,8 +241,8 @@ table (`sphinx.directives.patches.RSTTable`): <emphasis> title <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> diff --git a/tests/test_renderers/fixtures/tables.md b/tests/test_renderers/fixtures/tables.md index f24a86cc..b478b926 100644 --- a/tests/test_renderers/fixtures/tables.md +++ b/tests/test_renderers/fixtures/tables.md @@ -7,8 +7,8 @@ a|b <document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> @@ -35,8 +35,8 @@ Header only: <document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> @@ -56,9 +56,9 @@ a | b | c <document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="3"> - <colspec colwidth="33.33"> - <colspec colwidth="33.33"> - <colspec colwidth="33.33"> + <colspec colwidth="33"> + <colspec colwidth="33"> + <colspec colwidth="33"> <thead> <row> <entry classes="text-left"> @@ -92,8 +92,8 @@ Nested syntax: <document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> @@ -125,8 +125,8 @@ a|b <document source="<src>/index.md"> <table classes="colwidths-auto"> <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> diff --git a/tests/test_sphinx/sourcedirs/texi_table/conf.py b/tests/test_sphinx/sourcedirs/texi_table/conf.py new file mode 100644 index 00000000..e1c5009b --- /dev/null +++ b/tests/test_sphinx/sourcedirs/texi_table/conf.py @@ -0,0 +1,2 @@ +extensions = ["myst_parser"] +exclude_patterns = ["_build"] diff --git a/tests/test_sphinx/sourcedirs/texi_table/index.md b/tests/test_sphinx/sourcedirs/texi_table/index.md new file mode 100644 index 00000000..9face4b5 --- /dev/null +++ b/tests/test_sphinx/sourcedirs/texi_table/index.md @@ -0,0 +1,3 @@ +| foo | bar | +| --- | --- | +| baz | bim | diff --git a/tests/test_sphinx/test_sphinx_builds.py b/tests/test_sphinx/test_sphinx_builds.py index e8615a64..ca34c733 100644 --- a/tests/test_sphinx/test_sphinx_builds.py +++ b/tests/test_sphinx/test_sphinx_builds.py @@ -547,3 +547,20 @@ def test_fieldlist_extension( regress_html=True, regress_ext=f".sphinx{sphinx.version_info[0]}.html", ) + + +@pytest.mark.sphinx( + buildername="texinfo", + srcdir=os.path.join(SOURCE_DIR, "texi_table"), + freshenv=True, +) +def test_texinfo_table( + app, + status, + warning, +): + """Test that tables can be built with the Texinfo builder.""" + app.build() + assert "build succeeded" in status.getvalue() # Build succeeded + warnings = warning.getvalue().strip() + assert warnings == "" diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx4.xml index ed086997..8a12765e 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx4.xml +++ b/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx4.xml @@ -75,8 +75,8 @@ a=1{`} <table classes="colwidths-auto"> <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx4.xml index c793d318..34b0e3c3 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx4.xml +++ b/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx4.xml @@ -76,8 +76,8 @@ a=1{`} <table classes="colwidths-auto"> <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> + <colspec colwidth="50"> + <colspec colwidth="50"> <thead> <row> <entry> diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx4.xml index 1b1c195b..231ca337 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx4.xml +++ b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx4.xml @@ -43,7 +43,7 @@ gras <table classes="colwidths-auto"> <tgroup cols="1"> - <colspec colwidth="100.0"> + <colspec colwidth="100"> <thead> <row> <entry> diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx4.xml index 0d8dbc79..dfc5f414 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx4.xml +++ b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx4.xml @@ -43,7 +43,7 @@ gras <table classes="colwidths-auto"> <tgroup cols="1"> - <colspec colwidth="100.0"> + <colspec colwidth="100"> <thead> <row> <entry> From 2e91dd85a4deaf997a79e07bc130d22271c0207f Mon Sep 17 00:00:00 2001 From: Chris Sewell <chrisj_sewell@hotmail.com> Date: Tue, 7 Jun 2022 11:55:36 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Drop=20Sphinx=203,=20a?= =?UTF-8?q?dd=20Sphinx=205=20(#579)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 15 +- .pre-commit-config.yaml | 2 +- myst_parser/_compat.py | 11 + myst_parser/mdit_to_docutils/base.py | 9 +- myst_parser/sphinx_ext/myst_refs.py | 4 +- pyproject.toml | 5 +- .../fixtures/sphinx_directives.md | 16 +- tests/test_renderers/test_fixtures_sphinx.py | 11 +- tests/test_sphinx/conftest.py | 4 +- tests/test_sphinx/test_sphinx_builds.py | 47 ++-- .../test_basic.resolved.sphinx3.xml | 154 ------------- ...ed.sphinx4.xml => test_basic.resolved.xml} | 0 .../test_sphinx_builds/test_basic.sphinx3.xml | 155 ------------- ...c.sphinx3.html => test_basic.sphinx5.html} | 36 +-- ...{test_basic.sphinx4.xml => test_basic.xml} | 0 ...sphinx4.html => test_commonmark_only.html} | 2 +- .../test_commonmark_only.sphinx3.html | 28 --- ...hinx4.html => test_extended_syntaxes.html} | 2 +- .../test_extended_syntaxes.sphinx3.html | 173 -------------- .../test_extended_syntaxes.sphinx3.xml | 90 -------- ...sphinx4.xml => test_extended_syntaxes.xml} | 0 .../test_fieldlist_extension.sphinx3.xml | 82 ------- ... => test_fieldlist_extension.sphinx5.html} | 30 ++- ...hinx4.xml => test_fieldlist_extension.xml} | 0 .../test_footnotes.sphinx3.html | 147 ------------ .../test_footnotes.sphinx5.html | 211 ++++++++++++++++++ ...t_gettext.sphinx4.pot => test_gettext.pot} | 0 .../test_gettext.sphinx3.pot | 69 ------ ...ot => test_gettext_additional_targets.pot} | 0 ...est_gettext_additional_targets.sphinx3.pot | 126 ----------- .../test_gettext_html.resolved.sphinx3.xml | 93 -------- ...nx4.xml => test_gettext_html.resolved.xml} | 0 .../test_gettext_html.sphinx3.xml | 93 -------- ...x3.html => test_gettext_html.sphinx5.html} | 16 +- ...html.sphinx4.xml => test_gettext_html.xml} | 0 ...hinx4.html => test_heading_slug_func.html} | 4 +- .../test_heading_slug_func.sphinx3.html | 22 -- ...cludes.sphinx4.html => test_includes.html} | 8 +- .../test_includes.sphinx3.html | 131 ----------- .../test_includes.sphinx3.xml | 113 ---------- ...includes.sphinx4.xml => test_includes.xml} | 0 ...nces.sphinx4.html => test_references.html} | 4 +- .../test_references.sphinx3.html | 195 ---------------- ...4.html => test_references_singlehtml.html} | 8 +- .../test_references_singlehtml.sphinx3.html | 111 --------- tox.ini | 7 +- 46 files changed, 343 insertions(+), 1891 deletions(-) create mode 100644 myst_parser/_compat.py delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_basic.resolved.sphinx4.xml => test_basic.resolved.xml} (100%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_basic.sphinx3.html => test_basic.sphinx5.html} (89%) rename tests/test_sphinx/test_sphinx_builds/{test_basic.sphinx4.xml => test_basic.xml} (100%) rename tests/test_sphinx/test_sphinx_builds/{test_commonmark_only.sphinx4.html => test_commonmark_only.html} (97%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx3.html rename tests/test_sphinx/test_sphinx_builds/{test_extended_syntaxes.sphinx4.html => test_extended_syntaxes.html} (99%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.html delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_extended_syntaxes.sphinx4.xml => test_extended_syntaxes.xml} (100%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_fieldlist_extension.sphinx3.html => test_fieldlist_extension.sphinx5.html} (83%) rename tests/test_sphinx/test_sphinx_builds/{test_fieldlist_extension.sphinx4.xml => test_fieldlist_extension.xml} (100%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx3.html create mode 100644 tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx5.html rename tests/test_sphinx/test_sphinx_builds/{test_gettext.sphinx4.pot => test_gettext.pot} (100%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_gettext.sphinx3.pot rename tests/test_sphinx/test_sphinx_builds/{test_gettext_additional_targets.sphinx4.pot => test_gettext_additional_targets.pot} (100%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.sphinx3.pot delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_gettext_html.resolved.sphinx4.xml => test_gettext_html.resolved.xml} (100%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_gettext_html.sphinx3.html => test_gettext_html.sphinx5.html} (91%) rename tests/test_sphinx/test_sphinx_builds/{test_gettext_html.sphinx4.xml => test_gettext_html.xml} (100%) rename tests/test_sphinx/test_sphinx_builds/{test_heading_slug_func.sphinx4.html => test_heading_slug_func.html} (93%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx3.html rename tests/test_sphinx/test_sphinx_builds/{test_includes.sphinx4.html => test_includes.html} (97%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.html delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.xml rename tests/test_sphinx/test_sphinx_builds/{test_includes.sphinx4.xml => test_includes.xml} (100%) rename tests/test_sphinx/test_sphinx_builds/{test_references.sphinx4.html => test_references.html} (98%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_references.sphinx3.html rename tests/test_sphinx/test_sphinx_builds/{test_references_singlehtml.sphinx4.html => test_references_singlehtml.html} (96%) delete mode 100644 tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx3.html diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4bbe10c5..4000d5da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,16 +26,15 @@ jobs: fail-fast: false matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] - sphinx: [">=4,<5"] + sphinx: [">=5,<6"] os: [ubuntu-latest] include: - # fails because of: https://github.com/sphinx-doc/sphinx/issues/10291 - # - os: ubuntu-latest - # python-version: "3.8" - # sphinx: ">=3,<4" - - os: windows-latest - python-version: "3.8" - sphinx: ">=4,<5" + - os: ubuntu-latest + python-version: "3.8" + sphinx: ">=4,<5" + - os: windows-latest + python-version: "3.8" + sphinx: ">=4,<5" runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0ca0fc9..316a10dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: - id: mypy args: [--config-file=pyproject.toml] additional_dependencies: - - sphinx~=4.1 + - sphinx~=5.0 - markdown-it-py>=1.0.0,<3.0.0 - mdit-py-plugins~=0.3.0 files: > diff --git a/myst_parser/_compat.py b/myst_parser/_compat.py new file mode 100644 index 00000000..d29cf4d8 --- /dev/null +++ b/myst_parser/_compat.py @@ -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) diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py index 252cc353..49c3fd22 100644 --- a/myst_parser/mdit_to_docutils/base.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -32,6 +32,7 @@ from markdown_it.token import Token from markdown_it.tree import SyntaxTreeNode +from myst_parser._compat import findall from myst_parser.config.main import MdParserConfig from myst_parser.mocking import ( MockIncludeDirective, @@ -41,7 +42,7 @@ MockState, MockStateMachine, ) -from ..parsers.directives import DirectiveParsingError, parse_directive_text +from myst_parser.parsers.directives import DirectiveParsingError, parse_directive_text from .html_to_nodes import html_to_nodes from .utils import is_external_url @@ -260,7 +261,7 @@ def _render_finalise(self) -> None: # those from the initial markdown parse # instead we gather them from a walk of the created document foot_refs = OrderedDict() - for refnode in self.document.traverse(nodes.footnote_reference): + for refnode in findall(self.document)(nodes.footnote_reference): if refnode["refname"] not in foot_refs: foot_refs[refnode["refname"]] = True @@ -447,7 +448,7 @@ def render_inline(self, token: SyntaxTreeNode) -> None: self.render_children(token) def render_text(self, token: SyntaxTreeNode) -> None: - self.current_node.append(nodes.Text(token.content, token.content)) + self.current_node.append(nodes.Text(token.content)) def render_bullet_list(self, token: SyntaxTreeNode) -> None: list_node = nodes.bullet_list() @@ -888,7 +889,7 @@ def dict_to_fm_field_list( field_node = nodes.field() field_node.source = value - field_node += nodes.field_name(key, "", nodes.Text(key, key)) + field_node += nodes.field_name(key, "", nodes.Text(key)) field_node += nodes.field_body(value, *[body]) field_list += field_node diff --git a/myst_parser/sphinx_ext/myst_refs.py b/myst_parser/sphinx_ext/myst_refs.py index 81675a1b..9ba223d6 100644 --- a/myst_parser/sphinx_ext/myst_refs.py +++ b/myst_parser/sphinx_ext/myst_refs.py @@ -16,6 +16,8 @@ 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: @@ -35,7 +37,7 @@ class MystReferenceResolver(ReferencesResolver): def run(self, **kwargs: Any) -> None: self.document: document - for node in self.document.traverse(addnodes.pending_xref): + for node in findall(self.document)(addnodes.pending_xref): if node["reftype"] != "myst": continue diff --git a/pyproject.toml b/pyproject.toml index 487d334e..a2e988ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,12 +34,12 @@ keywords = [ ] requires-python = ">=3.7" dependencies = [ - "docutils>=0.15,<0.18", + "docutils>=0.15,<0.19", "jinja2", # required for substitutions, but let sphinx choose version "markdown-it-py>=1.0.0,<3.0.0", "mdit-py-plugins~=0.3.0", "pyyaml", - "sphinx>=3.1,<5", + "sphinx>=4,<6", "typing-extensions", ] @@ -63,7 +63,6 @@ rtd = [ testing = [ "beautifulsoup4", "coverage[toml]", - "docutils~=0.17.0", # this version changes some HTML tags "pytest>=6,<7", "pytest-cov", "pytest-regressions", diff --git a/tests/test_renderers/fixtures/sphinx_directives.md b/tests/test_renderers/fixtures/sphinx_directives.md index 2084558e..2b924572 100644 --- a/tests/test_renderers/fixtures/sphinx_directives.md +++ b/tests/test_renderers/fixtures/sphinx_directives.md @@ -14,7 +14,7 @@ default-domain (`sphinx.directives.DefaultDomain`): <document source="<src>/index.md"> . -SPHINX4 object (`sphinx.directives.ObjectDescription`): +object (`sphinx.directives.ObjectDescription`): . ```{object} something ``` @@ -161,7 +161,7 @@ acks (`sphinx.directives.other.Acks`): name . -SPHINX4 hlist (`sphinx.directives.other.HList`): +hlist (`sphinx.directives.other.HList`): . ```{hlist} @@ -386,18 +386,18 @@ term 2 : B Definition of both terms. . -SPHINX3 productionlist (`sphinx.domains.std.ProductionList`): +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 grammar-token-try-stmt" tokenname="try_stmt" xml:space="preserve"> + <production ids="grammar-token-try_stmt" tokenname="try_stmt" xml:space="preserve"> try1_stmt | try2_stmt . -SPHINX4 cmdoption (`sphinx.domains.std.Cmdoption`): +cmdoption (`sphinx.domains.std.Cmdoption`): . ```{cmdoption} a ``` @@ -412,7 +412,7 @@ SPHINX4 cmdoption (`sphinx.domains.std.Cmdoption`): <desc_content> . -SPHINX4 rst:directive (`sphinx.domains.rst.ReSTDirective`): +rst:directive (`sphinx.domains.rst.ReSTDirective`): . ```{rst:directive} a ``` @@ -426,7 +426,7 @@ SPHINX4 rst:directive (`sphinx.domains.rst.ReSTDirective`): <desc_content> . -SPHINX4 rst:directive:option (`sphinx.domains.rst.ReSTDirectiveOption`): +SPHINX4-SKIP rst:directive:option (`sphinx.domains.rst.ReSTDirectiveOption`): . ```{rst:directive:option} a ``` @@ -434,7 +434,7 @@ SPHINX4 rst:directive:option (`sphinx.domains.rst.ReSTDirectiveOption`): <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 directive:option--a"> + <desc_signature classes="sig sig-object" ids="directive-option-a"> <desc_name classes="sig-name descname" xml:space="preserve"> :a: <desc_content> diff --git a/tests/test_renderers/test_fixtures_sphinx.py b/tests/test_renderers/test_fixtures_sphinx.py index abb57aa1..b8cf5497 100644 --- a/tests/test_renderers/test_fixtures_sphinx.py +++ b/tests/test_renderers/test_fixtures_sphinx.py @@ -9,7 +9,6 @@ from pathlib import Path import pytest -import sphinx from sphinx_pytest.plugin import CreateDoctree from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer @@ -42,11 +41,9 @@ def test_directive_options(file_params, sphinx_doctree_no_tr: CreateDoctree): 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"): - pytest.skip(file_params.title) - elif file_params.title.startswith("SPHINX3") and sphinx.version_info[0] < 3: - pytest.skip(file_params.title) - elif file_params.title.startswith("SPHINX4") and sphinx.version_info[0] < 4: + 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"]}) @@ -63,8 +60,6 @@ def test_sphinx_directives(file_params, sphinx_doctree_no_tr: CreateDoctree): def test_sphinx_roles(file_params, sphinx_doctree_no_tr: CreateDoctree): if file_params.title.startswith("SKIP"): pytest.skip(file_params.title) - elif file_params.title.startswith("SPHINX4") and sphinx.version_info[0] < 4: - 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") diff --git a/tests/test_sphinx/conftest.py b/tests/test_sphinx/conftest.py index e7fb75a8..4165a318 100644 --- a/tests/test_sphinx/conftest.py +++ b/tests/test_sphinx/conftest.py @@ -39,6 +39,8 @@ def test_basic(app, status, warning, get_sphinx_app_output): from bs4 import BeautifulSoup from sphinx.testing.path import path +from myst_parser._compat import findall + SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "sourcedirs")) @@ -109,7 +111,7 @@ def read( extension = regress_ext # convert absolute filenames - for node in doctree.traverse( + for node in findall(doctree)( lambda n: "source" in n and not isinstance(n, str) ): node["source"] = pathlib.Path(node["source"]).name diff --git a/tests/test_sphinx/test_sphinx_builds.py b/tests/test_sphinx/test_sphinx_builds.py index ca34c733..c30850f8 100644 --- a/tests/test_sphinx/test_sphinx_builds.py +++ b/tests/test_sphinx/test_sphinx_builds.py @@ -37,19 +37,19 @@ def test_basic( warnings = warning.getvalue().strip() assert warnings == "" - get_sphinx_app_doctree( - app, - docname="content", - regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", - ) - get_sphinx_app_doctree( - app, - docname="content", - resolve=True, - regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", - ) + try: + get_sphinx_app_doctree( + app, + docname="content", + regress=True, + ) + finally: + get_sphinx_app_doctree( + app, + docname="content", + resolve=True, + regress=True, + ) get_sphinx_app_output( app, filename="content.html", @@ -104,7 +104,7 @@ def test_references( app, filename="index.html", regress_html=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.html", + replace={"Permalink to this headline": "Permalink to this heading"}, ) @@ -154,7 +154,7 @@ def test_references_singlehtml( filename="index.html", buildername="singlehtml", regress_html=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.html", + replace={"Permalink to this headline": "Permalink to this heading"}, ) @@ -185,7 +185,7 @@ def test_heading_slug_func( app, filename="index.html", regress_html=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.html", + replace={"Permalink to this headline": "Permalink to this heading"}, ) @@ -216,14 +216,13 @@ def test_extended_syntaxes( app, docname="index", regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", ) finally: get_sphinx_app_output( app, filename="index.html", regress_html=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.html", + replace={"Permalink to this headline": "Permalink to this heading"}, ) @@ -249,7 +248,6 @@ def test_includes( app, docname="index", regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", # fix for Windows CI replace={ r"subfolder\example2.jpg": "subfolder/example2.jpg", @@ -262,8 +260,8 @@ def test_includes( app, filename="index.html", regress_html=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.html", replace={ + "Permalink to this headline": "Permalink to this heading", r"'subfolder\\example2'": "'subfolder/example2'", r'uri="subfolder\\example2"': 'uri="subfolder/example2"', "_images/example21.jpg": "_images/example2.jpg", @@ -354,7 +352,7 @@ def test_commonmark_only( app, filename="index.html", regress_html=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.html", + replace={"Permalink to this headline": "Permalink to this heading"}, ) @@ -407,7 +405,7 @@ def test_gettext( output = re.sub(r"POT-Creation-Date: [0-9: +-]+", "POT-Creation-Date: ", output) output = re.sub(r"Copyright \(C\) [0-9]{4}", "Copyright (C) XXXX", output) - file_regression.check(output, extension=f".sphinx{sphinx.version_info[0]}.pot") + file_regression.check(output, extension=".pot") @pytest.mark.sphinx( @@ -434,7 +432,6 @@ def test_gettext_html( app, docname="index", regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", ) finally: get_sphinx_app_doctree( @@ -442,7 +439,6 @@ def test_gettext_html( docname="index", resolve=True, regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", ) get_sphinx_app_output( app, @@ -483,7 +479,7 @@ def test_gettext_additional_targets( output = re.sub(r"POT-Creation-Date: [0-9: +-]+", "POT-Creation-Date: ", output) output = re.sub(r"Copyright \(C\) [0-9]{4}", "Copyright (C) XXXX", output) - file_regression.check(output, extension=f".sphinx{sphinx.version_info[0]}.pot") + file_regression.check(output, extension=".pot") @pytest.mark.sphinx( @@ -530,7 +526,6 @@ def test_fieldlist_extension( app, docname="index", regress=True, - regress_ext=f".sphinx{sphinx.version_info[0]}.xml", # changed in: # https://www.sphinx-doc.org/en/master/changes.html#release-4-4-0-released-jan-17-2022 replace={ diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx3.xml deleted file mode 100644 index 762dc6bd..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx3.xml +++ /dev/null @@ -1,154 +0,0 @@ -<document source="content.md"> - <topic classes="dedication"> - <title> - Dedication - <paragraph> - To my - <emphasis> - homies - - <topic classes="abstract"> - <title> - Abstract - <paragraph> - Something something - <strong> - dark - side - <target refid="target"> - <section classes="tex2jax_ignore mathjax_ignore" ids="header target" names="header target"> - <title> - Header - <comment xml:space="preserve"> - comment - <note> - <paragraph> - abcd - <emphasis> - abc - - <reference refuri="https://www.google.com"> - google - <warning> - <paragraph> - xyz - <admonition classes="admonition-title-with-link-target2"> - <title> - Title with - <reference internal="True" refid="target2"> - <inline classes="std std-ref"> - link - <paragraph> - Content - <target refid="target2"> - <figure align="default" ids="id1 target2" names="target2"> - <reference refuri="https://www.google.com"> - <image candidates="{'*': 'example.jpg'}" height="40px" uri="example.jpg"> - <caption> - Caption - <paragraph> - <image alt="alternative text" candidates="{'*': 'example.jpg'}" uri="example.jpg"> - <paragraph> - <reference refuri="https://www.google.com"> - https://www.google.com - <paragraph> - <strong> - <literal classes="code"> - a=1{`} - <paragraph> - <math> - sdfds - <paragraph> - <strong> - <math> - a=1 - <math_block nowrap="False" number="True" xml:space="preserve"> - b=2 - <target refid="equation-eq-label"> - <math_block docname="content" ids="equation-eq-label" label="eq:label" nowrap="False" number="1" xml:space="preserve"> - c=2 - <paragraph> - <reference internal="True" refid="equation-eq-label"> - (1) - <paragraph> - <literal> - a=1{`} - <table align="default" classes="colwidths-auto"> - <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> - <thead> - <row> - <entry> - <paragraph> - a - <entry classes="text-right"> - <paragraph> - b - <tbody> - <row> - <entry> - <paragraph> - <emphasis> - a - <entry classes="text-right"> - <paragraph> - 2 - <row> - <entry> - <paragraph> - <reference refuri="https://google.com"> - link-a - <entry classes="text-right"> - <paragraph> - <reference refuri="https://python.org"> - link-b - <paragraph> - this - - is - - a - - paragraph - <comment xml:space="preserve"> - a comment 2 - <paragraph> - this is a second paragraph - <bullet_list bullet="-"> - <list_item> - <paragraph> - a list - <bullet_list bullet="-"> - <list_item> - <paragraph> - a sub list - <comment xml:space="preserve"> - a comment 3 - <bullet_list bullet="-"> - <list_item> - <paragraph> - new list? - <paragraph> - <reference internal="True" refid="target"> - <inline classes="std std-ref"> - Header - - <reference internal="True" refid="target2"> - <inline classes="std std-ref"> - Caption - <comment classes="block_break" xml:space="preserve"> - a block break - <paragraph> - <reference refuri="https://www.google.com" title="a title"> - name - <literal_block language="default" linenos="False" xml:space="preserve"> - def func(a, b=1): - print(a) - <paragraph> - Special substitution references: - <paragraph> - 57 - words | - 0 - min read diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_basic.resolved.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_basic.resolved.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_basic.resolved.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.xml deleted file mode 100644 index 775a420e..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.xml +++ /dev/null @@ -1,155 +0,0 @@ -<document source="content.md"> - <topic classes="dedication"> - <title> - Dedication - <paragraph> - To my - <emphasis> - homies - - <topic classes="abstract"> - <title> - Abstract - <paragraph> - Something something - <strong> - dark - side - <target refid="target"> - <section classes="tex2jax_ignore mathjax_ignore" ids="header target" names="header target"> - <title> - Header - <comment xml:space="preserve"> - comment - <note> - <paragraph> - abcd - <emphasis> - abc - - <reference refuri="https://www.google.com"> - google - <warning> - <paragraph> - xyz - <admonition classes="admonition-title-with-link-target2"> - <title> - Title with - <pending_xref refdoc="content" refdomain="True" refexplicit="True" reftarget="target2" reftype="myst" refwarn="True"> - <inline classes="xref myst"> - link - <paragraph> - Content - <target refid="target2"> - <figure align="default" ids="id1 target2" names="target2"> - <reference refuri="https://www.google.com"> - <image candidates="{'*': 'example.jpg'}" height="40px" uri="example.jpg"> - <caption> - Caption - <paragraph> - <image alt="alternative text" candidates="{'*': 'example.jpg'}" uri="example.jpg"> - <paragraph> - <reference refuri="https://www.google.com"> - https://www.google.com - <paragraph> - <strong> - <literal classes="code"> - a=1{`} - <paragraph> - <math> - sdfds - <paragraph> - <strong> - <math> - a=1 - <math_block nowrap="False" number="True" xml:space="preserve"> - b=2 - <target refid="equation-eq-label"> - <math_block docname="content" ids="equation-eq-label" label="eq:label" nowrap="False" number="1" xml:space="preserve"> - c=2 - <paragraph> - <pending_xref refdoc="content" refdomain="math" refexplicit="False" reftarget="eq:label" reftype="eq" refwarn="True"> - <literal classes="xref eq"> - eq:label - <paragraph> - <literal> - a=1{`} - <table align="default" classes="colwidths-auto"> - <tgroup cols="2"> - <colspec colwidth="50.0"> - <colspec colwidth="50.0"> - <thead> - <row> - <entry> - <paragraph> - a - <entry classes="text-right"> - <paragraph> - b - <tbody> - <row> - <entry> - <paragraph> - <emphasis> - a - <entry classes="text-right"> - <paragraph> - 2 - <row> - <entry> - <paragraph> - <reference refuri="https://google.com"> - link-a - <entry classes="text-right"> - <paragraph> - <reference refuri="https://python.org"> - link-b - <paragraph> - this - - is - - a - - paragraph - <comment xml:space="preserve"> - a comment 2 - <paragraph> - this is a second paragraph - <bullet_list bullet="-"> - <list_item> - <paragraph> - a list - <bullet_list bullet="-"> - <list_item> - <paragraph> - a sub list - <comment xml:space="preserve"> - a comment 3 - <bullet_list bullet="-"> - <list_item> - <paragraph> - new list? - <paragraph> - <pending_xref refdoc="content" refdomain="std" refexplicit="False" reftarget="target" reftype="ref" refwarn="True"> - <inline classes="xref std std-ref"> - target - - <pending_xref refdoc="content" refdomain="std" refexplicit="False" reftarget="target2" reftype="ref" refwarn="True"> - <inline classes="xref std std-ref"> - target2 - <comment classes="block_break" xml:space="preserve"> - a block break - <paragraph> - <reference refuri="https://www.google.com" title="a title"> - name - <literal_block language="default" xml:space="preserve"> - def func(a, b=1): - print(a) - <paragraph> - Special substitution references: - <paragraph> - 57 - words | - 0 - min read diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx5.html similarity index 89% rename from tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.html rename to tests/test_sphinx/test_sphinx_builds/test_basic.sphinx5.html index 36dd8c84..9813b705 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx3.html +++ b/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx5.html @@ -1,7 +1,7 @@ <div class="documentwrapper"> <div class="bodywrapper"> <div class="body" role="main"> - <div class="dedication topic"> + <div class="topic dedication" role="doc-dedication"> <p class="topic-title"> Dedication </p> @@ -12,7 +12,7 @@ </em> </p> </div> - <div class="abstract topic"> + <div class="topic abstract" role="doc-abstract"> <p class="topic-title"> Abstract </p> @@ -24,12 +24,12 @@ side </p> </div> - <div class="tex2jax_ignore mathjax_ignore section" id="header"> + <section class="tex2jax_ignore mathjax_ignore" id="header"> <span id="target"> </span> <h1> Header - <a class="headerlink" href="#header" title="Permalink to this headline"> + <a class="headerlink" href="#header" title="Permalink to this heading"> ¶ </a> </h1> @@ -68,21 +68,23 @@ <h1> Content </p> </div> - <div class="figure align-default" id="id1"> + <figure class="align-default" id="id1"> <span id="target2"> </span> <a class="reference external image-reference" href="https://www.google.com"> <img alt="_images/example.jpg" src="_images/example.jpg" style="height: 40px;"/> </a> - <p class="caption"> - <span class="caption-text"> - Caption - </span> - <a class="headerlink" href="#id1" title="Permalink to this image"> - ¶ - </a> - </p> - </div> + <figcaption> + <p> + <span class="caption-text"> + Caption + </span> + <a class="headerlink" href="#id1" title="Permalink to this image"> + ¶ + </a> + </p> + </figcaption> + </figure> <p> <img alt="alternative text" src="_images/example.jpg"/> </p> @@ -136,7 +138,7 @@ <h1> </span> </code> </p> - <table class="colwidths-auto docutils align-default"> + <table class="docutils align-default"> <thead> <tr class="row-odd"> <th class="head"> @@ -144,7 +146,7 @@ <h1> a </p> </th> - <th class="text-right head"> + <th class="head text-right"> <p> b </p> @@ -244,7 +246,7 @@ <h1> <p> 57 words | 0 min read </p> - </div> + </section> </div> </div> </div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_basic.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_basic.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_basic.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_basic.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx4.html b/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.html similarity index 97% rename from tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx4.html rename to tests/test_sphinx/test_sphinx_builds/test_commonmark_only.html index 294bc663..381996f7 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx4.html +++ b/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.html @@ -4,7 +4,7 @@ <section id="test"> <h1> Test - <a class="headerlink" href="#test" title="Permalink to this headline"> + <a class="headerlink" href="#test" title="Permalink to this heading"> ¶ </a> </h1> diff --git a/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx3.html deleted file mode 100644 index b6d79f68..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_commonmark_only.sphinx3.html +++ /dev/null @@ -1,28 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="section" id="test"> - <h1> - Test - <a class="headerlink" href="#test" title="Permalink to this headline"> - ¶ - </a> - </h1> - <div class="highlight-{note} notranslate"> - <div class="highlight"> - <pre><span></span>hallo -</pre> - </div> - </div> - <p> - {a} - <code class="docutils literal notranslate"> - <span class="pre"> - b - </span> - </code> - </p> - </div> - </div> - </div> -</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx4.html b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html similarity index 99% rename from tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx4.html rename to tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html index 082eb20c..cc6e85b5 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx4.html +++ b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html @@ -4,7 +4,7 @@ <section class="tex2jax_ignore mathjax_ignore" id="test"> <h1> Test - <a class="headerlink" href="#test" title="Permalink to this headline"> + <a class="headerlink" href="#test" title="Permalink to this heading"> ¶ </a> </h1> diff --git a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.html deleted file mode 100644 index b1ff5caa..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.html +++ /dev/null @@ -1,173 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="tex2jax_ignore mathjax_ignore section" id="test"> - <h1> - Test - <a class="headerlink" href="#test" title="Permalink to this headline"> - ¶ - </a> - </h1> - <p> - *disabled* - </p> - <p> - <span class="math notranslate nohighlight"> - \(a=1\) - </span> - </p> - <div class="math notranslate nohighlight"> - \[x=5\] - </div> - <div class="math notranslate nohighlight" id="equation-2"> - <span class="eqno"> - (1) - <a class="headerlink" href="#equation-2" title="Permalink to this equation"> - ¶ - </a> - </span> - \[x=5\] - </div> - <p> - $ a=1 $ - </p> - <p> - a - <div class="math notranslate nohighlight"> - \[c=3\] - </div> - b - </p> - <div class="amsmath math notranslate nohighlight" id="equation-mock-uuid"> - <span class="eqno"> - (2) - <a class="headerlink" href="#equation-mock-uuid" title="Permalink to this equation"> - ¶ - </a> - </span> - \[\begin{equation} -b=2 -\end{equation}\] - </div> - <div class="math notranslate nohighlight"> - \[ \begin{align}\begin{aligned}c=3\\d=4\end{aligned}\end{align} \] - </div> - <dl class="simple myst"> - <dt> - Term **1** - </dt> - <dd> - <p> - Definition *1* - </p> - <p> - second paragraph - </p> - </dd> - <dt> - Term 2 - </dt> - <dd> - <p> - Definition 2a - </p> - </dd> - <dd> - <p> - Definition 2b - </p> - </dd> - <dt> - Term 3 - </dt> - <dd> - <div class="highlight-none notranslate"> - <div class="highlight"> - <pre><span></span>code block -</pre> - </div> - </div> - </dd> - <dd> - <blockquote> - <div> - <p> - quote - </p> - </div> - </blockquote> - </dd> - <dd> - <p> - other - </p> - </dd> - </dl> - <div class="other figure align-default" id="target"> - <img alt="fun-fish" src="_images/fun-fish.png"/> - <p class="caption"> - <span class="caption-text"> - This is a caption in **Markdown** - </span> - <a class="headerlink" href="#target" title="Permalink to this image"> - ¶ - </a> - </p> - </div> - <div class="other figure align-default" id="other-target"> - <a class="bg-primary mb-1 reference internal image-reference" href="_images/fun-fish.png"> - <img alt="fishy" class="bg-primary mb-1" src="_images/fun-fish.png" style="width: 200px;"/> - </a> - <p class="caption"> - <span class="caption-text"> - This is a caption in **Markdown** - </span> - <a class="headerlink" href="#other-target" title="Permalink to this image"> - ¶ - </a> - </p> - </div> - <p> - linkify URL: - <a class="reference external" href="http://www.example.com"> - www.example.com - </a> - </p> - <ul class="contains-task-list simple"> - <li class="task-list-item"> - <p> - <input class="task-list-item-checkbox" disabled="disabled" type="checkbox"/> - hallo - </p> - </li> - <li class="task-list-item"> - <p> - <input checked="checked" class="task-list-item-checkbox" disabled="disabled" type="checkbox"/> - there - </p> - </li> - </ul> - <p> - Numbered code block: - </p> - <div class="highlight-typescript notranslate"> - <table class="highlighttable"> - <tr> - <td class="linenos"> - <div class="linenodiv"> - <pre><span class="normal">1</span></pre> - </div> - </td> - <td class="code"> - <div class="highlight"> - <pre><span></span><span class="kr">type</span> <span class="nx">Result</span> <span class="o">=</span> <span class="s2">"pass"</span> <span class="o">|</span> <span class="s2">"fail"</span> -</pre> - </div> - </td> - </tr> - </table> - </div> - </div> - </div> - </div> -</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.xml deleted file mode 100644 index 18802ecc..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx3.xml +++ /dev/null @@ -1,90 +0,0 @@ -<document source="index.md"> - <meta content="meta description" lang="en" name="description"> - <meta content="en_US" property="og:locale"> - <section classes="tex2jax_ignore mathjax_ignore" ids="test" names="test"> - <title> - Test - <paragraph> - *disabled* - <paragraph> - <math> - a=1 - <math_block nowrap="False" number="True" xml:space="preserve"> - x=5 - <target refid="equation-2"> - <math_block docname="index" ids="equation-2" label="2" nowrap="False" number="1" xml:space="preserve"> - x=5 - <paragraph> - $ a=1 $ - <paragraph> - a - <math_block nowrap="False" number="True" xml:space="preserve"> - c=3 - b - <target refid="equation-mock-uuid"> - <math_block classes="amsmath" docname="index" ids="equation-mock-uuid" label="mock-uuid" nowrap="True" number="2" xml:space="preserve"> - \begin{equation} - b=2 - \end{equation} - <math_block docname="index" label="True" nowrap="False" number="True" xml:space="preserve"> - c=3 - - d=4 - <definition_list classes="simple myst"> - <definition_list_item> - <term> - Term **1** - <definition> - <paragraph> - Definition *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 - <figure align="default" classes="other" ids="target" names="target"> - <image alt="fun-fish" candidates="{'*': 'fun-fish.png'}" uri="fun-fish.png"> - <caption> - This is a caption in **Markdown** - <figure align="default" classes="other" ids="other-target" names="other-target"> - <image alt="fishy" candidates="{'*': 'fun-fish.png'}" classes="bg-primary mb-1" uri="fun-fish.png" width="200px"> - <caption> - This is a caption in **Markdown** - <paragraph> - linkify URL: - <reference refuri="http://www.example.com"> - www.example.com - <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"> - hallo - <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"> - there - <paragraph> - Numbered code block: - <literal_block language="typescript" linenos="True" xml:space="preserve"> - type Result = "pass" | "fail" diff --git a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.xml deleted file mode 100644 index 9ce67bb3..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.xml +++ /dev/null @@ -1,82 +0,0 @@ -<document source="index.md"> - <section ids="test" names="test"> - <title> - Test - <field_list classes="myst"> - <field> - <field_name> - field - <field_body> - <field> - <field_name> - <emphasis> - field - <field_body> - <paragraph> - content - <index entries="('pair',\ 'built-in\ function;\ send_message()',\ 'send_message',\ '',\ None)"> - <desc classes="py" desctype="function" domain="py" noindex="False" objtype="function"> - <desc_signature class="" fullname="send_message" ids="send_message" module="True"> - <desc_name xml:space="preserve"> - send_message - <desc_parameterlist xml:space="preserve"> - <desc_parameter xml:space="preserve"> - <desc_sig_name classes="n"> - sender - <desc_parameter xml:space="preserve"> - <desc_sig_name classes="n"> - priority - <desc_content> - <paragraph> - Send a message to a recipient - <field_list classes="myst"> - <field> - <field_name> - Parameters - <field_body> - <bullet_list> - <list_item> - <paragraph> - <literal_strong> - sender - ( - <pending_xref py:class="True" py:module="True" refdomain="py" refexplicit="False" refspecific="True" reftarget="str" reftype="class"> - <literal_emphasis> - str - ) - – - The person sending the message - <list_item> - <paragraph> - <literal_strong> - priority - ( - <pending_xref py:class="True" py:module="True" refdomain="py" refexplicit="False" refspecific="True" reftarget="int" reftype="class"> - <literal_emphasis> - int - ) - – - The priority of the message, can be a number 1-5 - <field> - <field_name> - Returns - <field_body> - <paragraph> - the message id - <field> - <field_name> - Return type - <field_body> - <paragraph> - <pending_xref py:class="True" py:module="True" refdomain="py" refexplicit="False" refspecific="True" reftarget="int" reftype="class"> - int - <field> - <field_name> - Raises - <field_body> - <paragraph> - <pending_xref py:class="True" py:module="True" refdomain="py" refexplicit="False" refspecific="True" reftarget="ValueError" reftype="exc"> - <literal_strong> - ValueError - – - if the message_body exceeds 160 characters diff --git a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx5.html similarity index 83% rename from tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.html rename to tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx5.html index 0d29ed85..8548f3d6 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx3.html +++ b/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx5.html @@ -1,16 +1,19 @@ <div class="documentwrapper"> <div class="bodywrapper"> <div class="body" role="main"> - <div class="section" id="test"> + <section id="test"> <h1> Test - <a class="headerlink" href="#test" title="Permalink to this headline"> + <a class="headerlink" href="#test" title="Permalink to this heading"> ¶ </a> </h1> <dl class="myst field-list simple"> <dt class="field-odd"> field + <span class="colon"> + : + </span> </dt> <dd class="field-odd"> <p> @@ -20,6 +23,9 @@ <h1> <em> field </em> + <span class="colon"> + : + </span> </dt> <dd class="field-even"> <p> @@ -28,12 +34,12 @@ <h1> </dd> </dl> <dl class="py function"> - <dt id="send_message"> - <code class="sig-name descname"> + <dt class="sig sig-object py" id="send_message"> + <span class="sig-name descname"> <span class="pre"> send_message </span> - </code> + </span> <span class="sig-paren"> ( </span> @@ -66,6 +72,9 @@ <h1> <dl class="myst field-list simple"> <dt class="field-odd"> Parameters + <span class="colon"> + : + </span> </dt> <dd class="field-odd"> <ul class="simple"> @@ -97,6 +106,9 @@ <h1> </dd> <dt class="field-even"> Returns + <span class="colon"> + : + </span> </dt> <dd class="field-even"> <p> @@ -105,6 +117,9 @@ <h1> </dd> <dt class="field-odd"> Return type + <span class="colon"> + : + </span> </dt> <dd class="field-odd"> <p> @@ -113,6 +128,9 @@ <h1> </dd> <dt class="field-even"> Raises + <span class="colon"> + : + </span> </dt> <dd class="field-even"> <p> @@ -125,7 +143,7 @@ <h1> </dl> </dd> </dl> - </div> + </section> </div> </div> </div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx3.html deleted file mode 100644 index fb0714c2..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx3.html +++ /dev/null @@ -1,147 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="section" id="footnotes-with-markdown"> - <h1> - Footnotes with Markdown - <a class="headerlink" href="#footnotes-with-markdown" title="Permalink to this headline"> - ¶ - </a> - </h1> - <p> - <a class="footnote-reference brackets" href="#c" id="id1"> - 1 - </a> - </p> - <div class="admonition note"> - <p class="admonition-title"> - Note - </p> - <p> - <a class="footnote-reference brackets" href="#d" id="id2"> - 2 - </a> - </p> - </div> - <p> - <a class="footnote-reference brackets" href="#a" id="id3"> - 3 - </a> - </p> - <p> - <a class="footnote-reference brackets" href="#b" id="id4"> - 4 - </a> - </p> - <p> - <a class="footnote-reference brackets" href="#id8" id="id5"> - 123 - </a> - <a class="footnote-reference brackets" href="#id8" id="id6"> - 123 - </a> - </p> - <p> - <a class="footnote-reference brackets" href="#e" id="id7"> - 5 - </a> - </p> - <blockquote> - <div> - <ul class="simple"> - <li> - </li> - </ul> - </div> - </blockquote> - <hr class="footnotes docutils"/> - <dl class="footnote brackets"> - <dt class="label" id="c"> - <span class="brackets"> - <a class="fn-backref" href="#id1"> - 1 - </a> - </span> - </dt> - <dd> - <p> - a footnote referenced first - </p> - </dd> - <dt class="label" id="d"> - <span class="brackets"> - <a class="fn-backref" href="#id2"> - 2 - </a> - </span> - </dt> - <dd> - <p> - a footnote referenced in a directive - </p> - </dd> - <dt class="label" id="a"> - <span class="brackets"> - <a class="fn-backref" href="#id3"> - 3 - </a> - </span> - </dt> - <dd> - <p> - some footnote - <em> - text - </em> - </p> - </dd> - <dt class="label" id="b"> - <span class="brackets"> - <a class="fn-backref" href="#id4"> - 4 - </a> - </span> - </dt> - <dd> - <p> - a footnote before its reference - </p> - </dd> - <dt class="label" id="id8"> - <span class="brackets"> - 123 - </span> - <span class="fn-backref"> - ( - <a href="#id5"> - 1 - </a> - , - <a href="#id6"> - 2 - </a> - ) - </span> - </dt> - <dd> - <p> - multiple references footnote - </p> - </dd> - <dt class="label" id="e"> - <span class="brackets"> - <a class="fn-backref" href="#id7"> - 5 - </a> - </span> - </dt> - <dd> - <p> - footnote definition in a block element - </p> - </dd> - </dl> - </div> - </div> - </div> -</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx5.html b/tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx5.html new file mode 100644 index 00000000..9129e236 --- /dev/null +++ b/tests/test_sphinx/test_sphinx_builds/test_footnotes.sphinx5.html @@ -0,0 +1,211 @@ +<div class="documentwrapper"> + <div class="bodywrapper"> + <div class="body" role="main"> + <section id="footnotes-with-markdown"> + <h1> + Footnotes with Markdown + <a class="headerlink" href="#footnotes-with-markdown" title="Permalink to this heading"> + ¶ + </a> + </h1> + <p> + <a class="footnote-reference brackets" href="#c" id="id1" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 1 + <span class="fn-bracket"> + ] + </span> + </a> + </p> + <div class="admonition note"> + <p class="admonition-title"> + Note + </p> + <p> + <a class="footnote-reference brackets" href="#d" id="id2" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 2 + <span class="fn-bracket"> + ] + </span> + </a> + </p> + </div> + <p> + <a class="footnote-reference brackets" href="#a" id="id3" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 3 + <span class="fn-bracket"> + ] + </span> + </a> + </p> + <p> + <a class="footnote-reference brackets" href="#b" id="id4" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 4 + <span class="fn-bracket"> + ] + </span> + </a> + </p> + <p> + <a class="footnote-reference brackets" href="#id8" id="id5" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 123 + <span class="fn-bracket"> + ] + </span> + </a> + <a class="footnote-reference brackets" href="#id8" id="id6" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 123 + <span class="fn-bracket"> + ] + </span> + </a> + </p> + <p> + <a class="footnote-reference brackets" href="#e" id="id7" role="doc-noteref"> + <span class="fn-bracket"> + [ + </span> + 5 + <span class="fn-bracket"> + ] + </span> + </a> + </p> + <blockquote> + <div> + <ul class="simple"> + <li> + </li> + </ul> + </div> + </blockquote> + <hr class="footnotes docutils"/> + <aside class="footnote brackets" id="c" role="note"> + <span class="label"> + <span class="fn-bracket"> + [ + </span> + <a href="#id1" role="doc-backlink"> + 1 + </a> + <span class="fn-bracket"> + ] + </span> + </span> + <p> + a footnote referenced first + </p> + </aside> + <aside class="footnote brackets" id="d" role="note"> + <span class="label"> + <span class="fn-bracket"> + [ + </span> + <a href="#id2" role="doc-backlink"> + 2 + </a> + <span class="fn-bracket"> + ] + </span> + </span> + <p> + a footnote referenced in a directive + </p> + </aside> + <aside class="footnote brackets" id="a" role="note"> + <span class="label"> + <span class="fn-bracket"> + [ + </span> + <a href="#id3" role="doc-backlink"> + 3 + </a> + <span class="fn-bracket"> + ] + </span> + </span> + <p> + some footnote + <em> + text + </em> + </p> + </aside> + <aside class="footnote brackets" id="b" role="note"> + <span class="label"> + <span class="fn-bracket"> + [ + </span> + <a href="#id4" role="doc-backlink"> + 4 + </a> + <span class="fn-bracket"> + ] + </span> + </span> + <p> + a footnote before its reference + </p> + </aside> + <aside class="footnote brackets" id="id8" role="note"> + <span class="label"> + <span class="fn-bracket"> + [ + </span> + 123 + <span class="fn-bracket"> + ] + </span> + </span> + <span class="backrefs"> + ( + <a href="#id5" role="doc-backlink"> + 1 + </a> + , + <a href="#id6" role="doc-backlink"> + 2 + </a> + ) + </span> + <p> + multiple references footnote + </p> + </aside> + <aside class="footnote brackets" id="e" role="note"> + <span class="label"> + <span class="fn-bracket"> + [ + </span> + <a href="#id7" role="doc-backlink"> + 5 + </a> + <span class="fn-bracket"> + ] + </span> + </span> + <p> + footnote definition in a block element + </p> + </aside> + </section> + </div> + </div> +</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext.sphinx4.pot b/tests/test_sphinx/test_sphinx_builds/test_gettext.pot similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_gettext.sphinx4.pot rename to tests/test_sphinx/test_sphinx_builds/test_gettext.pot diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext.sphinx3.pot b/tests/test_sphinx/test_sphinx_builds/test_gettext.sphinx3.pot deleted file mode 100644 index 5f767603..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext.sphinx3.pot +++ /dev/null @@ -1,69 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) XXXX, Executable Book Project -# This file is distributed under the same license as the Python package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: Python \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: \n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../../index.md:1 -msgid "**bold** text 1" -msgstr "" - -#: ../../index.md:3 -msgid "**bold** text 2" -msgstr "" - -#: ../../index.md:5 -msgid "**bold** text 3" -msgstr "" - -#: ../../index.md:10 -msgid "**bold** text 4" -msgstr "" - -#: ../../index.md:13 -msgid "**bold** text 5" -msgstr "" - -#: ../../index.md:15 -msgid "**bold** text 6" -msgstr "" - -#: ../../index.md:17 -msgid "**bold** text 7" -msgstr "" - -#: ../../index.md:18 -msgid "**bold** text 8" -msgstr "" - -#: ../../index.md:0 -msgid "**bold** text 9" -msgstr "" - -#: ../../index.md:0 -msgid "**bold** text 10" -msgstr "" - -#: ../../index.md:26 -msgid "**bold** text 11" -msgstr "" - -#: ../../index.md:28 -msgid "Extra ```backticks```" -msgstr "" - -#: ../../index.md:55 -msgid "![Fun Fish 1](fun-fish.png)" -msgstr "" diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.sphinx4.pot b/tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.pot similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.sphinx4.pot rename to tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.pot diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.sphinx3.pot b/tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.sphinx3.pot deleted file mode 100644 index 839d877b..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext_additional_targets.sphinx3.pot +++ /dev/null @@ -1,126 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) XXXX, Executable Book Project -# This file is distributed under the same license as the Python package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: Python \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: \n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../../index.md:1 -msgid "**bold** text 1" -msgstr "" - -#: ../../index.md:3 -msgid "**bold** text 2" -msgstr "" - -#: ../../index.md:5 -msgid "**bold** text 3" -msgstr "" - -#: ../../index.md:10 -msgid "**bold** text 4" -msgstr "" - -#: ../../index.md:13 -msgid "**bold** text 5" -msgstr "" - -#: ../../index.md:15 -msgid "**bold** text 6" -msgstr "" - -#: ../../index.md:17 -msgid "**bold** text 7" -msgstr "" - -#: ../../index.md:18 -msgid "**bold** text 8" -msgstr "" - -#: ../../index.md:0 -msgid "**bold** text 9" -msgstr "" - -#: ../../index.md:0 -msgid "**bold** text 10" -msgstr "" - -#: ../../index.md:24 -msgid "<div markdown=1>\n" -"" -msgstr "" - -#: ../../index.md:26 -msgid "**bold** text 11" -msgstr "" - -#: ../../index.md:28 -msgid "Extra ```backticks```" -msgstr "" - -#: ../../index.md:30 -msgid "</div>\n" -"" -msgstr "" - -#: ../../index.md:32 -msgid "**additional** text 12\n" -"" -msgstr "" - -#: ../../index.md:34 -msgid "**additional** text 13\n" -"" -msgstr "" - -#: ../../index.md:38 -msgid "{\n" -" \"additional\": \"text 14\"\n" -"}\n" -"" -msgstr "" - -#: ../../index.md:44 -msgid "<h3>**additional** text 15</h3>\n" -"" -msgstr "" - -#: ../../index.md:46 -msgid ">>> print('doctest block')\n" -"doctest block\n" -"" -msgstr "" - -#: ../../index.md:51 -msgid "<iframe src=\"http://sphinx-doc.org\"></iframe>" -msgstr "" - -#: ../../index.md:55 -msgid "![Fun Fish 1](fun-fish.png)" -msgstr "" - -#: ../../index.md:55 -msgid ".. image:: fun-fish.png\n" -" :alt: Fun Fish 1" -msgstr "" - -#: ../../index.md:57 -msgid ".. image:: fun-fish.png\n" -" :alt: Fun Fish 2" -msgstr "" - -#: ../../index.md:61 -msgid ".. image:: fun-fish.png\n" -" :alt: Fun Fish 3" -msgstr "" diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx3.xml deleted file mode 100644 index 8d9866e6..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx3.xml +++ /dev/null @@ -1,93 +0,0 @@ -<document source="index.md"> - <section ids="bold-text-1" names="bold\ text\ 1 texte\ 1\ en\ gras"> - <title> - texte 1 en - <strong> - gras - <paragraph> - texte 2 en - <strong> - gras - <block_quote> - <paragraph> - texte 3 en - <strong> - gras - <note> - <paragraph> - texte 4 en - <strong> - gras - <bullet_list bullet="*"> - <list_item> - <paragraph> - texte 5 en - <strong> - gras - <enumerated_list enumtype="arabic" prefix="" suffix="."> - <list_item> - <paragraph> - texte 6 en - <strong> - gras - <definition_list classes="simple myst"> - <definition_list_item> - <term> - texte 7 en - <strong> - gras - <definition> - <paragraph> - texte 8 en - <strong> - gras - <table align="default" classes="colwidths-auto"> - <tgroup cols="1"> - <colspec colwidth="100.0"> - <thead> - <row> - <entry> - <paragraph> - texte 9 en - <strong> - gras - <tbody> - <row> - <entry> - <paragraph> - texte 10 en - <strong> - gras - <raw format="html" xml:space="preserve"> - <div markdown=1> - <paragraph> - texte 11 en - <strong> - gras - <paragraph> - «  - <literal> - Backtick -  » supplémentaire - <raw format="html" xml:space="preserve"> - </div> - <literal_block language="none" linenos="False" xml:space="preserve"> - **additional** text 12 - <literal_block language="default" linenos="False" xml:space="preserve"> - **additional** text 13 - <literal_block language="json" linenos="False" xml:space="preserve"> - { - "additional": "text 14" - } - <raw format="html" xml:space="preserve"> - <h3>**additional** text 15</h3> - <literal_block language="python" linenos="False" xml:space="preserve"> - >>> print('doctest block') - doctest block - <raw format="html" xml:space="preserve"> - <iframe src="http://sphinx-doc.org"></iframe> - <paragraph> - <image alt="Poisson amusant 1" candidates="{'*': 'poisson-amusant.png'}" uri="poisson-amusant.png"> - <image alt="Fun Fish 2" candidates="{'*': 'fun-fish.png'}" uri="fun-fish.png"> - <figure align="default"> - <image alt="Fun Fish 3" candidates="{'*': 'fun-fish.png'}" uri="fun-fish.png"> diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_gettext_html.resolved.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.xml deleted file mode 100644 index ef749eb6..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.xml +++ /dev/null @@ -1,93 +0,0 @@ -<document source="index.md"> - <section ids="bold-text-1" names="bold\ text\ 1 texte\ 1\ en\ gras"> - <title> - texte 1 en - <strong> - gras - <paragraph> - texte 2 en - <strong> - gras - <block_quote> - <paragraph> - texte 3 en - <strong> - gras - <note> - <paragraph> - texte 4 en - <strong> - gras - <bullet_list bullet="*"> - <list_item> - <paragraph> - texte 5 en - <strong> - gras - <enumerated_list enumtype="arabic" prefix="" suffix="."> - <list_item> - <paragraph> - texte 6 en - <strong> - gras - <definition_list classes="simple myst"> - <definition_list_item> - <term> - texte 7 en - <strong> - gras - <definition> - <paragraph> - texte 8 en - <strong> - gras - <table align="default" classes="colwidths-auto"> - <tgroup cols="1"> - <colspec colwidth="100.0"> - <thead> - <row> - <entry> - <paragraph> - texte 9 en - <strong> - gras - <tbody> - <row> - <entry> - <paragraph> - texte 10 en - <strong> - gras - <raw format="html" xml:space="preserve"> - <div markdown=1> - <paragraph> - texte 11 en - <strong> - gras - <paragraph> - «  - <literal> - Backtick -  » supplémentaire - <raw format="html" xml:space="preserve"> - </div> - <literal_block language="none" xml:space="preserve"> - **additional** text 12 - <literal_block language="default" xml:space="preserve"> - **additional** text 13 - <literal_block language="json" xml:space="preserve"> - { - "additional": "text 14" - } - <raw format="html" xml:space="preserve"> - <h3>**additional** text 15</h3> - <literal_block language="python" xml:space="preserve"> - >>> print('doctest block') - doctest block - <raw format="html" xml:space="preserve"> - <iframe src="http://sphinx-doc.org"></iframe> - <paragraph> - <image alt="Poisson amusant 1" candidates="{'*': 'poisson-amusant.png'}" uri="poisson-amusant.png"> - <image alt="Fun Fish 2" candidates="{'*': 'fun-fish.png'}" uri="fun-fish.png"> - <figure align="default"> - <image alt="Fun Fish 3" candidates="{'*': 'fun-fish.png'}" uri="fun-fish.png"> diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx5.html similarity index 91% rename from tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.html rename to tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx5.html index 1c1f8615..469e1882 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx3.html +++ b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx5.html @@ -1,13 +1,13 @@ <div class="documentwrapper"> <div class="bodywrapper"> <div class="body" role="main"> - <div class="section" id="bold-text-1"> + <section id="bold-text-1"> <h1> texte 1 en <strong> gras </strong> - <a class="headerlink" href="#bold-text-1" title="Lien permanent vers ce titre"> + <a class="headerlink" href="#bold-text-1" title="Lien permanent vers cette rubrique"> ¶ </a> </h1> @@ -74,7 +74,7 @@ <h1> </p> </dd> </dl> - <table class="colwidths-auto docutils align-default"> + <table class="docutils align-default"> <thead> <tr class="row-odd"> <th class="head"> @@ -152,11 +152,11 @@ <h3> <p> <img alt="Poisson amusant 1" src="_images/poisson-amusant.png"/> </p> - <img alt="Fun Fish 2" src="_images/fun-fish.png"/> - <div class="figure align-default"> - <img alt="Fun Fish 3" src="_images/fun-fish.png"/> - </div> - </div> + <img alt="Poisson amusant 2" src="_images/fun-fish.png"/> + <figure class="align-default"> + <img alt="Poisson amusant 3" src="_images/fun-fish.png"/> + </figure> + </section> </div> </div> </div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_gettext_html.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_gettext_html.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_gettext_html.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx4.html b/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.html similarity index 93% rename from tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx4.html rename to tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.html index b3631a0d..67c44157 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx4.html +++ b/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.html @@ -4,14 +4,14 @@ <section id="hyphen-1"> <h1> Hyphen - 1 - <a class="headerlink" href="#hyphen-1" title="Permalink to this headline"> + <a class="headerlink" href="#hyphen-1" title="Permalink to this heading"> ¶ </a> </h1> <section id="dot-1-1"> <h2> Dot 1.1 - <a class="headerlink" href="#dot-1-1" title="Permalink to this headline"> + <a class="headerlink" href="#dot-1-1" title="Permalink to this heading"> ¶ </a> </h2> diff --git a/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx3.html deleted file mode 100644 index cbf5e049..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.sphinx3.html +++ /dev/null @@ -1,22 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="section" id="hyphen-1"> - <h1> - Hyphen - 1 - <a class="headerlink" href="#hyphen-1" title="Permalink to this headline"> - ¶ - </a> - </h1> - <div class="section" id="dot-1-1"> - <h2> - Dot 1.1 - <a class="headerlink" href="#dot-1-1" title="Permalink to this headline"> - ¶ - </a> - </h2> - </div> - </div> - </div> - </div> -</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx4.html b/tests/test_sphinx/test_sphinx_builds/test_includes.html similarity index 97% rename from tests/test_sphinx/test_sphinx_builds/test_includes.sphinx4.html rename to tests/test_sphinx/test_sphinx_builds/test_includes.html index 78662234..41eb1ee6 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx4.html +++ b/tests/test_sphinx/test_sphinx_builds/test_includes.html @@ -4,7 +4,7 @@ <section id="main-title"> <h1> Main Title - <a class="headerlink" href="#main-title" title="Permalink to this headline"> + <a class="headerlink" href="#main-title" title="Permalink to this heading"> ¶ </a> </h1> @@ -13,7 +13,7 @@ <h1> </span> <h2> A Sub-Heading in Include - <a class="headerlink" href="#a-sub-heading-in-include" title="Permalink to this headline"> + <a class="headerlink" href="#a-sub-heading-in-include" title="Permalink to this heading"> ¶ </a> </h2> @@ -27,7 +27,7 @@ <h2> <section id="a-sub-heading-in-nested-include"> <h2> A Sub-Heading in Nested Include - <a class="headerlink" href="#a-sub-heading-in-nested-include" title="Permalink to this headline"> + <a class="headerlink" href="#a-sub-heading-in-nested-include" title="Permalink to this heading"> ¶ </a> </h2> @@ -120,7 +120,7 @@ <h2> <section id="a-sub-sub-heading"> <h3> A Sub-sub-Heading - <a class="headerlink" href="#a-sub-sub-heading" title="Permalink to this headline"> + <a class="headerlink" href="#a-sub-sub-heading" title="Permalink to this heading"> ¶ </a> </h3> diff --git a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.html deleted file mode 100644 index d3f22324..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.html +++ /dev/null @@ -1,131 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="section" id="main-title"> - <h1> - Main Title - <a class="headerlink" href="#main-title" title="Permalink to this headline"> - ¶ - </a> - </h1> - <div class="section" id="a-sub-heading-in-include"> - <span id="inc-header"> - </span> - <h2> - A Sub-Heading in Include - <a class="headerlink" href="#a-sub-heading-in-include" title="Permalink to this headline"> - ¶ - </a> - </h2> - <p> - Some text with - <em> - syntax - </em> - </p> - </div> - <div class="section" id="a-sub-heading-in-nested-include"> - <h2> - A Sub-Heading in Nested Include - <a class="headerlink" href="#a-sub-heading-in-nested-include" title="Permalink to this headline"> - ¶ - </a> - </h2> - <p> - Some other text with - <strong> - syntax - </strong> - </p> - <p> - This relative path will refer to the importing file: - </p> - <div class="figure align-default" id="id1"> - <img alt="_images/example1.jpg" src="_images/example1.jpg"/> - <p class="caption"> - <span class="caption-text"> - Caption - </span> - <a class="headerlink" href="#id1" title="Permalink to this image"> - ¶ - </a> - </p> - </div> - <p> - This absolute path will refer to the project root (where the - <code class="docutils literal notranslate"> - <span class="pre"> - conf.py - </span> - </code> - is): - </p> - <div class="figure align-default" id="id2"> - <img alt="_images/example2.jpg" src="_images/example2.jpg"/> - <p class="caption"> - <span class="caption-text"> - Caption - </span> - <a class="headerlink" href="#id2" title="Permalink to this image"> - ¶ - </a> - </p> - </div> - <p> - <img alt="alt" src="_images/example2.jpg"/> - </p> - <p> - <img alt="alt" src="https://example.com"/> - </p> - <p> - <a class="reference internal" href="#"> - <span class="doc std std-doc"> - text - </span> - </a> - </p> - <p> - <a class="reference internal" href="#inc-header"> - <span class="std std-ref"> - A Sub-Heading in Include - </span> - </a> - </p> - <div class="code python highlight-default notranslate"> - <div class="highlight"> - <pre><span></span><span class="k">def</span> <span class="nf">a_func</span><span class="p">(</span><span class="n">param</span><span class="p">):</span> - <span class="nb">print</span><span class="p">(</span><span class="n">param</span><span class="p">)</span> -</pre> - </div> - </div> - <pre class="code python literal-block"><code><span class="ln">0 </span><span class="keyword">def</span> <span class="name function">a_func</span><span class="punctuation">(</span><span class="name">param</span><span class="punctuation">):</span> -<span class="ln">1 </span> <span class="name builtin">print</span><span class="punctuation">(</span><span class="name">param</span><span class="punctuation">)</span></code></pre> - <div class="highlight-default notranslate"> - <div class="highlight"> - <pre><span></span><span class="n">This</span> <span class="n">should</span> <span class="n">be</span> <span class="o">*</span><span class="n">literal</span><span class="o">*</span> - -<span class="n">Lots</span> -<span class="n">of</span> -<span class="n">lines</span> -<span class="n">so</span> <span class="n">we</span> <span class="n">can</span> <span class="n">select</span> <span class="n">some</span> -</pre> - </div> - </div> - <pre class="literal-block" id="literal-ref"><span class="ln">0 </span>Lots -<span class="ln">1 </span>of</pre> - <div class="section" id="a-sub-sub-heading"> - <h3> - A Sub-sub-Heading - <a class="headerlink" href="#a-sub-sub-heading" title="Permalink to this headline"> - ¶ - </a> - </h3> - <p> - some more text - </p> - </div> - </div> - </div> - </div> - </div> -</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.xml b/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.xml deleted file mode 100644 index aee98fee..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx3.xml +++ /dev/null @@ -1,113 +0,0 @@ -<document source="index.md"> - <section ids="main-title" names="main\ title"> - <title> - Main Title - <target refid="inc-header"> - <section ids="a-sub-heading-in-include inc-header" names="a\ sub-heading\ in\ include inc_header"> - <title> - A Sub-Heading in Include - <paragraph> - Some text with - <emphasis> - syntax - <section ids="a-sub-heading-in-nested-include" names="a\ sub-heading\ in\ nested\ include"> - <title> - A Sub-Heading in Nested Include - <paragraph> - Some other text with - <strong> - syntax - <paragraph> - This relative path will refer to the importing file: - <figure align="default" ids="id1"> - <image candidates="{'*': 'example1.jpg'}" uri="example1.jpg"> - <caption> - Caption - <paragraph> - This absolute path will refer to the project root (where the - <literal> - conf.py - is): - <figure align="default" ids="id2"> - <image candidates="{'*': 'subfolder/example2.jpg'}" uri="subfolder/example2.jpg"> - <caption> - Caption - <paragraph> - <image alt="alt" candidates="{'*': 'subfolder/example2.jpg'}" uri="subfolder/example2.jpg"> - <paragraph> - <image alt="alt" candidates="{'?': 'https://example.com'}" uri="https://example.com"> - <paragraph> - <pending_xref refdoc="index" refdomain="True" refexplicit="True" reftarget="index.md" reftype="myst" refwarn="True"> - <inline classes="xref myst"> - text - <paragraph> - <pending_xref refdoc="index" refdomain="std" refexplicit="False" reftarget="inc_header" reftype="ref" refwarn="True"> - <inline classes="xref std std-ref"> - inc_header - <literal_block classes="code python" source="include_code.py" xml:space="preserve"> - <inline classes="keyword"> - def - - <inline classes="name function"> - a_func - <inline classes="punctuation"> - ( - <inline classes="name"> - param - <inline classes="punctuation"> - ): - - - <inline classes="name builtin"> - print - <inline classes="punctuation"> - ( - <inline classes="name"> - param - <inline classes="punctuation"> - ) - <literal_block classes="code python" source="include_code.py" xml:space="preserve"> - <inline classes="ln"> - 0 - <inline classes="keyword"> - def - - <inline classes="name function"> - a_func - <inline classes="punctuation"> - ( - <inline classes="name"> - param - <inline classes="punctuation"> - ): - - <inline classes="ln"> - 1 - - <inline classes="name builtin"> - print - <inline classes="punctuation"> - ( - <inline classes="name"> - param - <inline classes="punctuation"> - ) - <literal_block source="include_literal.txt" xml:space="preserve"> - This should be *literal* - - Lots - of - lines - so we can select some - <literal_block ids="literal-ref" names="literal_ref" source="include_literal.txt" xml:space="preserve"> - <inline classes="ln"> - 0 - Lots - <inline classes="ln"> - 1 - of - <section ids="a-sub-sub-heading" names="a\ sub-sub-heading"> - <title> - A Sub-sub-Heading - <paragraph> - some more text diff --git a/tests/test_sphinx/test_sphinx_builds/test_includes.sphinx4.xml b/tests/test_sphinx/test_sphinx_builds/test_includes.xml similarity index 100% rename from tests/test_sphinx/test_sphinx_builds/test_includes.sphinx4.xml rename to tests/test_sphinx/test_sphinx_builds/test_includes.xml diff --git a/tests/test_sphinx/test_sphinx_builds/test_references.sphinx4.html b/tests/test_sphinx/test_sphinx_builds/test_references.html similarity index 98% rename from tests/test_sphinx/test_sphinx_builds/test_references.sphinx4.html rename to tests/test_sphinx/test_sphinx_builds/test_references.html index 677256e3..a6d4036f 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_references.sphinx4.html +++ b/tests/test_sphinx/test_sphinx_builds/test_references.html @@ -12,7 +12,7 @@ <h1> <span class="math notranslate nohighlight"> \(a=1\) </span> - <a class="headerlink" href="#title-with-nested-a-1" title="Permalink to this headline"> + <a class="headerlink" href="#title-with-nested-a-1" title="Permalink to this heading"> ¶ </a> </h1> @@ -129,7 +129,7 @@ <h2> <em> anchors </em> - <a class="headerlink" href="#title-anchors" title="Permalink to this headline"> + <a class="headerlink" href="#title-anchors" title="Permalink to this heading"> ¶ </a> </h2> diff --git a/tests/test_sphinx/test_sphinx_builds/test_references.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_references.sphinx3.html deleted file mode 100644 index acd217fd..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_references.sphinx3.html +++ /dev/null @@ -1,195 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="tex2jax_ignore mathjax_ignore section" id="title-with-nested-a-1"> - <span id="title"> - </span> - <h1> - Title with - <strong> - nested - </strong> - <span class="math notranslate nohighlight"> - \(a=1\) - </span> - <a class="headerlink" href="#title-with-nested-a-1" title="Permalink to this headline"> - ¶ - </a> - </h1> - <p> - <a class="reference external" href="https://example.com"> - </a> - </p> - <p> - <a class="reference external" href="https://example.com"> - plain text - </a> - </p> - <p> - <a class="reference external" href="https://example.com"> - nested - <em> - syntax - </em> - </a> - </p> - <p> - <a class="reference internal" href="#title"> - <span class="std std-ref"> - Title with nested a=1 - </span> - </a> - </p> - <p> - <a class="reference internal" href="#title"> - <span class="std std-ref"> - plain text - </span> - </a> - </p> - <p> - <a class="reference internal" href="#title"> - <span class="std std-ref"> - nested - <em> - syntax - </em> - </span> - </a> - </p> - <p> - <a class="reference internal" href="#"> - <span class="doc std std-doc"> - Title with nested a=1 - </span> - </a> - </p> - <p> - <a class="reference internal" href="#"> - <span class="doc std std-doc"> - plain text - </span> - </a> - </p> - <p> - <a class="reference internal" href="#"> - <span class="doc std std-doc"> - nested - <em> - syntax - </em> - </span> - </a> - </p> - <p> - <a class="reference download internal" download="" href="_downloads/ab0d698fdd2b6a81c34b5ed380fe6f61/file_link.txt"> - <span class="xref download myst"> - download - <strong> - link - </strong> - </span> - </a> - </p> - <p> - <a class="reference download internal" download="" href="_downloads/1952147a8403903cb78cecf56f049085/file_link2.txt"> - <span class="xref download myst"> - subfolder/file_link2.txt - </span> - </a> - </p> - <p id="insidecodeblock"> - I am inside the eval-rst fence - </p> - <p> - Referencing the - <a class="reference internal" href="#title"> - <span class="std std-ref"> - Title with nested a=1 - </span> - </a> - </p> - <p> - Still inside the codeblock - <a class="reference internal" href="#insidecodeblock"> - insidecodeblock - </a> - </p> - <p> - I am outside the - <a class="reference internal" href="#insidecodeblock"> - <span class="std std-ref"> - fence - </span> - </a> - </p> - <div class="section" id="title-anchors"> - <h2> - Title - <em> - anchors - </em> - <a class="headerlink" href="#title-anchors" title="Permalink to this headline"> - ¶ - </a> - </h2> - <div class="toctree-wrapper compound"> - <ul> - <li class="toctree-l1"> - <a class="reference internal" href="other.html"> - Title - <em> - anchors - </em> - </a> - </li> - <li class="toctree-l1"> - <a class="reference internal" href="subfolder/other2.html"> - Title - <em> - anchors - </em> - </a> - </li> - </ul> - </div> - <p> - <a class="reference internal" href="#title-anchors"> - <span class="std std-doc"> - Title anchors - </span> - </a> - </p> - <p> - <a class="reference internal" href="#title-anchors"> - <span class="std std-doc"> - Title anchors - </span> - </a> - </p> - <p> - <a class="reference internal" href="other.html#title-anchors"> - <span class="std std-doc"> - Title anchors - </span> - </a> - </p> - <p> - <a class="reference internal" href="other.html#title-anchors"> - <span class="std std-doc"> - Title anchors - </span> - </a> - </p> - <p> - <a class="reference internal" href="subfolder/other2.html#title-anchors"> - <span class="std std-doc"> - Title anchors - </span> - </a> - </p> - </div> - </div> - </div> - </div> -</div> diff --git a/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx4.html b/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.html similarity index 96% rename from tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx4.html rename to tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.html index 23ff49f9..b3d98a97 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx4.html +++ b/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.html @@ -4,7 +4,7 @@ <section id="title"> <h1> Title - <a class="headerlink" href="#title" title="Permalink to this headline"> + <a class="headerlink" href="#title" title="Permalink to this heading"> ¶ </a> </h1> @@ -14,7 +14,7 @@ <h1> <section id="other-index"> <h2> Other Index - <a class="headerlink" href="#other-index" title="Permalink to this headline"> + <a class="headerlink" href="#other-index" title="Permalink to this heading"> ¶ </a> </h2> @@ -24,7 +24,7 @@ <h2> <section id="other-title"> <h3> Other Title - <a class="headerlink" href="#other-title" title="Permalink to this headline"> + <a class="headerlink" href="#other-title" title="Permalink to this heading"> ¶ </a> </h3> @@ -62,7 +62,7 @@ <h3> <section id="other-2-title"> <h3> Other 2 Title - <a class="headerlink" href="#other-2-title" title="Permalink to this headline"> + <a class="headerlink" href="#other-2-title" title="Permalink to this heading"> ¶ </a> </h3> diff --git a/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx3.html b/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx3.html deleted file mode 100644 index 57f1e159..00000000 --- a/tests/test_sphinx/test_sphinx_builds/test_references_singlehtml.sphinx3.html +++ /dev/null @@ -1,111 +0,0 @@ -<div class="documentwrapper"> - <div class="bodywrapper"> - <div class="body" role="main"> - <div class="section" id="title"> - <h1> - Title - <a class="headerlink" href="#title" title="Permalink to this headline"> - ¶ - </a> - </h1> - <div class="toctree-wrapper compound"> - <span id="document-other/index"> - </span> - <div class="section" id="other-index"> - <h2> - Other Index - <a class="headerlink" href="#other-index" title="Permalink to this headline"> - ¶ - </a> - </h2> - <div class="toctree-wrapper compound"> - <span id="document-other/other"> - </span> - <div class="section" id="other-title"> - <h3> - Other Title - <a class="headerlink" href="#other-title" title="Permalink to this headline"> - ¶ - </a> - </h3> - <p> - <a class="reference internal" href="index.html#document-other/other2"> - <span class="doc"> - Other 2 Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="index.html#document-other/other2"> - <span class="doc"> - Other 2 Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="index.html#document-other/other2"> - <span class="doc std std-doc"> - Other 2 Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="index.html#title"> - <span class="std std-doc"> - Title - </span> - </a> - </p> - </div> - <span id="document-other/other2"> - </span> - <div class="section" id="other-2-title"> - <h3> - Other 2 Title - <a class="headerlink" href="#other-2-title" title="Permalink to this headline"> - ¶ - </a> - </h3> - </div> - </div> - </div> - </div> - <p> - <a class="reference internal" href="index.html#document-other/other"> - <span class="doc"> - Other Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="index.html#document-other/other"> - <span class="doc"> - Other Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="index.html#document-other/other"> - <span class="doc std std-doc"> - Other Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="#title"> - <span class="std std-doc"> - Title - </span> - </a> - </p> - <p> - <a class="reference internal" href="index.html#other-title"> - <span class="std std-doc"> - Other Title - </span> - </a> - </p> - </div> - </div> - </div> -</div> diff --git a/tox.ini b/tox.ini index 6c20b695..4a0110e6 100644 --- a/tox.ini +++ b/tox.ini @@ -11,12 +11,12 @@ # then then deleting compiled files has been found to fix it: `find . -name \*.pyc -delete` [tox] -envlist = py37-sphinx4 +envlist = py37-sphinx5 [testenv] usedevelop = true -[testenv:py{37,38,39,310}-sphinx{3,4}] +[testenv:py{37,38,39,310}-sphinx{4,5}] deps = black flake8 @@ -24,7 +24,7 @@ extras = linkify testing commands_pre = - sphinx3: pip install --quiet --upgrade-strategy "only-if-needed" "sphinx==3.5.4" + sphinx4: pip install --quiet --upgrade-strategy "only-if-needed" "sphinx==4.5.0" commands = pytest {posargs} [testenv:docs-{update,clean}] @@ -57,7 +57,6 @@ addopts = --ignore=setup.py markers = sphinx: set parameters for the sphinx `app` fixture (see ipypublish/sphinx/tests/conftest.py) filterwarnings = - ignore::DeprecationWarning:sphinx.jinja2glue.* [flake8] max-line-length = 100 From cc44a35273c78d1a613811d726ad05d014a27100 Mon Sep 17 00:00:00 2001 From: Chris Sewell <chrisj_sewell@hotmail.com> Date: Tue, 7 Jun 2022 12:40:57 +0200 Subject: [PATCH 5/7] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 316a10dc..7f4b90e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -45,7 +45,7 @@ repos: # - flake8-self~=0.2.2 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.961 hooks: - id: mypy args: [--config-file=pyproject.toml] From c17d855c48932dc37036740bfb1edf1b10ad4921 Mon Sep 17 00:00:00 2001 From: Chris Sewell <chrisj_sewell@hotmail.com> Date: Tue, 7 Jun 2022 14:00:27 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20`parse=5Fdirective=5F?= =?UTF-8?q?text`=20when=20body=20followed=20by=20options=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myst_parser/parsers/directives.py | 9 +- test.rst | 1 + .../fixtures/directive_parsing.txt | 141 ++++++++++++++++++ tests/test_renderers/test_parse_directives.py | 34 +++-- .../test_parsing_Note___class__name_n_na_.yml | 7 - .../test_parsing_Note__a_.yml | 5 - .../test_parsing_Note_a__.yml | 5 - 7 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 test.rst create mode 100644 tests/test_renderers/fixtures/directive_parsing.txt delete mode 100644 tests/test_renderers/test_parse_directives/test_parsing_Note___class__name_n_na_.yml delete mode 100644 tests/test_renderers/test_parse_directives/test_parsing_Note__a_.yml delete mode 100644 tests/test_renderers/test_parse_directives/test_parsing_Note_a__.yml diff --git a/myst_parser/parsers/directives.py b/myst_parser/parsers/directives.py index e275c843..56372545 100644 --- a/myst_parser/parsers/directives.py +++ b/myst_parser/parsers/directives.py @@ -78,13 +78,8 @@ def parse_directive_text( body_lines = content.splitlines() content_offset = 0 - if not ( - directive_class.required_arguments - or directive_class.optional_arguments - or options - ): - # If there are no possible arguments and no option block, - # then the body starts on the argument line + 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 = [] diff --git a/test.rst b/test.rst new file mode 100644 index 00000000..25855295 --- /dev/null +++ b/test.rst @@ -0,0 +1 @@ +.. note:: hallo diff --git a/tests/test_renderers/fixtures/directive_parsing.txt b/tests/test_renderers/fixtures/directive_parsing.txt new file mode 100644 index 00000000..35878bc4 --- /dev/null +++ b/tests/test_renderers/fixtures/directive_parsing.txt @@ -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 +. diff --git a/tests/test_renderers/test_parse_directives.py b/tests/test_renderers/test_parse_directives.py index 068a3d77..ae6792ba 100644 --- a/tests/test_renderers/test_parse_directives.py +++ b/tests/test_renderers/test_parse_directives.py @@ -1,27 +1,41 @@ -# TODO add more tests +from pathlib import Path + import pytest -from docutils.parsers.rst.directives.admonitions import Note +import yaml +from docutils.parsers.rst.directives.admonitions import Admonition, Note from docutils.parsers.rst.directives.body import Rubric +from markdown_it import MarkdownIt from myst_parser.parsers.directives import DirectiveParsingError, parse_directive_text +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures") -@pytest.mark.parametrize( - "klass,arguments,content", - [(Note, "", "a"), (Note, "a", ""), (Note, "", ":class: name\n\na")], -) -def test_parsing(klass, arguments, content, data_regression): + +@pytest.mark.param_file(FIXTURE_PATH / "directive_parsing.txt") +def test_parsing(file_params): + """Test parsing of directive text.""" + tokens = MarkdownIt("commonmark").parse(file_params.content) + assert len(tokens) == 1 and tokens[0].type == "fence" + name, *first_line = tokens[0].info.split(maxsplit=1) + if name == "{note}": + klass = Note + elif name == "{admonition}": + klass = Admonition + else: + raise AssertionError(f"Unknown directive: {name}") arguments, options, body_lines, content_offset = parse_directive_text( - klass, arguments, content + klass, first_line[0] if first_line else "", tokens[0].content ) - data_regression.check( + outcome = yaml.safe_dump( { "arguments": arguments, "options": options, "body": body_lines, "content_offset": content_offset, - } + }, + sort_keys=True, ) + file_params.assert_expected(outcome, rstrip_lines=True) @pytest.mark.parametrize( diff --git a/tests/test_renderers/test_parse_directives/test_parsing_Note___class__name_n_na_.yml b/tests/test_renderers/test_parse_directives/test_parsing_Note___class__name_n_na_.yml deleted file mode 100644 index 8b9bbdf3..00000000 --- a/tests/test_renderers/test_parse_directives/test_parsing_Note___class__name_n_na_.yml +++ /dev/null @@ -1,7 +0,0 @@ -arguments: [] -body: -- a -content_offset: 2 -options: - class: - - name diff --git a/tests/test_renderers/test_parse_directives/test_parsing_Note__a_.yml b/tests/test_renderers/test_parse_directives/test_parsing_Note__a_.yml deleted file mode 100644 index fd1e5585..00000000 --- a/tests/test_renderers/test_parse_directives/test_parsing_Note__a_.yml +++ /dev/null @@ -1,5 +0,0 @@ -arguments: [] -body: -- a -content_offset: 0 -options: {} diff --git a/tests/test_renderers/test_parse_directives/test_parsing_Note_a__.yml b/tests/test_renderers/test_parse_directives/test_parsing_Note_a__.yml deleted file mode 100644 index fd1e5585..00000000 --- a/tests/test_renderers/test_parse_directives/test_parsing_Note_a__.yml +++ /dev/null @@ -1,5 +0,0 @@ -arguments: [] -body: -- a -content_offset: 0 -options: {} From 75ef9cb7b65c98d969724fc6c096e8d6209c5ea0 Mon Sep 17 00:00:00 2001 From: Chris Sewell <chrisj_sewell@hotmail.com> Date: Tue, 7 Jun 2022 14:31:47 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=9A=80=20RELEASE:=200.18.0=20(#581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++++++++ myst_parser/__init__.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6598c57e..41d6743e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 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) diff --git a/myst_parser/__init__.py b/myst_parser/__init__.py index 507213b7..f3512379 100644 --- a/myst_parser/__init__.py +++ b/myst_parser/__init__.py @@ -1,5 +1,5 @@ """An extended commonmark compliant parser, with bridges to docutils & sphinx.""" -__version__ = "0.17.2" +__version__ = "0.18.0" def setup(app):