From 46fcdbc1379db98f79c3b732462aeea29c4ce684 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Fri, 31 Dec 2021 18:14:27 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20`toclist`=20extens?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 1 + docs/syntax/optional.md | 37 ++++++++ docs/syntax/subsection.md | 3 + myst_parser/docutils_.py | 2 + myst_parser/main.py | 17 +++- myst_parser/sphinx_renderer.py | 84 ++++++++++++++++++- tests/test_sphinx/sourcedirs/toclist/conf.py | 3 + tests/test_sphinx/sourcedirs/toclist/index.md | 7 ++ tests/test_sphinx/sourcedirs/toclist/page1.md | 1 + tests/test_sphinx/test_sphinx_builds.py | 25 ++++++ .../test_toclist_extension.xml | 10 +++ 11 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 docs/syntax/subsection.md create mode 100644 tests/test_sphinx/sourcedirs/toclist/conf.py create mode 100644 tests/test_sphinx/sourcedirs/toclist/index.md create mode 100644 tests/test_sphinx/sourcedirs/toclist/page1.md create mode 100644 tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml diff --git a/docs/conf.py b/docs/conf.py index f054c054..8434d94f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -87,6 +87,7 @@ "linkify", "substitution", "tasklist", + "toclist", ] myst_number_code_blocks = ["typescript"] myst_heading_anchors = 2 diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md index 03460473..d2fb8a9e 100644 --- a/docs/syntax/optional.md +++ b/docs/syntax/optional.md @@ -38,6 +38,7 @@ myst_enable_extensions = [ "smartquotes", "substitution", "tasklist", + "toclist", ] ``` @@ -682,6 +683,42 @@ Send a message to a recipient Currently `sphinx.ext.autodoc` does not support MyST, see [](howto/autodoc). ::: +(syntax/toclist)= +## Table of Contents Lists + +By adding `"toclist"` to `myst_enable_extensions` (in the sphinx `conf.py` [configuration file](https://www.sphinx-doc.org/en/master/usage/configuration.html)), +you will be able to specify [sphinx `toctree`](sphinx:toctree-directive) in a Markdown native manner. + +`toclist` are identified by bullet lists that use the `+` character as the marker, +and each item should be a link to another source file or an external hyperlink: + +```markdown ++ [Link text](subsection.md) ++ [Link text](https://example.com "Example external link") +``` + +is equivalent to: + +````markdown +```{toctree} + +subsection.md +Example external link +``` +```` + ++ [Link text](subsection.md) ++ [Link text](https://example.com "Example external link") + +Note that the link text is omitted from the output `toctree`, and the title of the link is taken from either the link title, if present, or the title of the source file. + +You can also specify the `maxdepth` and `numbered` options for all toclist in your `conf.py`: + +```python +myst_toclist_maxdepth = 2 +myst_toclist_numbered = True +``` + (syntax/images)= ## Images diff --git a/docs/syntax/subsection.md b/docs/syntax/subsection.md new file mode 100644 index 00000000..b63de12b --- /dev/null +++ b/docs/syntax/subsection.md @@ -0,0 +1,3 @@ +# Example subsection + +A subsection referenced by the `toclist` example. diff --git a/myst_parser/docutils_.py b/myst_parser/docutils_.py index 49502cd7..2dc181a5 100644 --- a/myst_parser/docutils_.py +++ b/myst_parser/docutils_.py @@ -65,6 +65,8 @@ def __repr__(self): "ref_domains", "update_mathjax", "mathjax_classes", + "toclist_maxdepth", + "toclist_numbered", ) """Names of settings that cannot be set in docutils.conf.""" diff --git a/myst_parser/main.py b/myst_parser/main.py index 30f98cc6..974268d5 100644 --- a/myst_parser/main.py +++ b/myst_parser/main.py @@ -89,9 +89,9 @@ def check_extensions(self, attribute, value): raise TypeError(f"myst_enable_extensions not iterable: {value}") diff = set(value).difference( [ - "dollarmath", "amsmath", "deflist", + "dollarmath", "fieldlist", "html_admonition", "html_image", @@ -101,6 +101,7 @@ def check_extensions(self, attribute, value): "linkify", "substitution", "tasklist", + "toclist", ] ) if diff: @@ -184,6 +185,18 @@ def check_extensions(self, attribute, value): default=("{", "}"), metadata={"help": "Substitution delimiters"} ) + toclist_maxdepth: Optional[int] = attr.ib( + default=None, + validator=optional(instance_of(int)), + metadata={"help": "Max depth of toctree created from the toclist extension"}, + ) + + toclist_numbered: bool = attr.ib( + default=False, + validator=instance_of(bool), + metadata={"help": "Number toctree entries created from the toclist extension"}, + ) + words_per_minute: int = attr.ib( default=200, validator=instance_of(int), @@ -307,6 +320,8 @@ def create_md_parser( "myst_footnote_transition": config.footnote_transition, "myst_number_code_blocks": config.number_code_blocks, "myst_highlight_code_blocks": config.highlight_code_blocks, + "myst_toclist_maxdepth": config.toclist_maxdepth, + "myst_toclist_numbered": config.toclist_numbered, } ) diff --git a/myst_parser/sphinx_renderer.py b/myst_parser/sphinx_renderer.py index 52118921..7f4bec25 100644 --- a/myst_parser/sphinx_renderer.py +++ b/myst_parser/sphinx_renderer.py @@ -10,6 +10,7 @@ from docutils import nodes from docutils.parsers.rst import directives, roles +from markdown_it.token import Token from markdown_it.tree import SyntaxTreeNode from sphinx import addnodes from sphinx.application import Sphinx, builtin_extensions @@ -25,7 +26,7 @@ from sphinx.util.nodes import clean_astext from sphinx.util.tags import Tags -from myst_parser.docutils_renderer import DocutilsRenderer +from myst_parser.docutils_renderer import DocutilsRenderer, token_line LOGGER = logging.getLogger(__name__) @@ -222,6 +223,87 @@ def add_math_target(self, node: nodes.math_block) -> nodes.target: self.document.note_explicit_target(target) return target + def render_bullet_list(self, token: SyntaxTreeNode) -> None: + # override for toclist extension + if ( + "toclist" not in self.config.get("myst_extensions", []) + or token.markup != "+" + ): + return super().render_bullet_list(token) + return self.render_toclist(token) + + def render_toclist(self, token: SyntaxTreeNode) -> None: + """Render a toclist as a sphinx ``toctree`` item.""" + # here we expect a list of links to documents/hyperlinks, + # with an optional title, e.g. + # `+ [discarded](doc.md "title")` + # print(token.pretty()) + # + # + # + # + # + # + items = [] + for child in token.children: + + # get the link + malformed_msg = "malformed toclist item, expected single link" + line = token_line(child, default=0) or None + if child.type != "list_item": + self.create_warning(malformed_msg, subtype="toclist", line=line) + continue + if len(child.children) != 1 or child.children[0].type != "paragraph": + self.create_warning(malformed_msg, subtype="toclist", line=line) + continue + if ( + len(child.children[0].children) != 1 + or child.children[0].children[0].type != "inline" + ): + self.create_warning(malformed_msg, subtype="toclist", line=line) + continue + if ( + len(child.children[0].children[0].children) != 1 + or child.children[0].children[0].children[0].type != "link" + ): + self.create_warning(malformed_msg, subtype="toclist", line=line) + continue + link = child.children[0].children[0].children[0] + + # add the toc item + href = cast(str, link.attrGet("href") or "") + if not href: + continue + title = link.attrGet("title") + # Note: we discard the link children since the toctree cannot use them + items.append({"href": href, "title": title}) + + if not items: + return + # we simply create the directive token and let sphinx handle generating the AST + content = "\n".join( + str(i["href"]) if not i["title"] else f"{i['title']} <{i['href']}>" + for i in items + ) + if self.config.get("myst_toclist_maxdepth", None) is not None: + content = f":maxdepth: {self.config['myst_toclist_maxdepth']}\n" + content + if self.config.get("myst_toclist_numbered", False): + content = ":numbered:\n" + content + toctree_token = SyntaxTreeNode( + tokens=[ + Token( + "fence", + "", + 0, + info="{toctree}", + map=cast(list, token.map), + content=content, + ) + ], + create_root=False, + ) + return self.render_directive(toctree_token) + def minimal_sphinx_app( configuration=None, sourcedir=None, with_builder=False, raise_on_warning=False diff --git a/tests/test_sphinx/sourcedirs/toclist/conf.py b/tests/test_sphinx/sourcedirs/toclist/conf.py new file mode 100644 index 00000000..2611c19b --- /dev/null +++ b/tests/test_sphinx/sourcedirs/toclist/conf.py @@ -0,0 +1,3 @@ +extensions = ["myst_parser"] +exclude_patterns = ["_build"] +myst_enable_extensions = ["toclist"] diff --git a/tests/test_sphinx/sourcedirs/toclist/index.md b/tests/test_sphinx/sourcedirs/toclist/index.md new file mode 100644 index 00000000..6cab203a --- /dev/null +++ b/tests/test_sphinx/sourcedirs/toclist/index.md @@ -0,0 +1,7 @@ +# Title + ++ [Some discarded text](page1.md) ++ [](https://example.com) ++ [](https://example.com "title") + +- A normal list diff --git a/tests/test_sphinx/sourcedirs/toclist/page1.md b/tests/test_sphinx/sourcedirs/toclist/page1.md new file mode 100644 index 00000000..0bb4fe87 --- /dev/null +++ b/tests/test_sphinx/sourcedirs/toclist/page1.md @@ -0,0 +1 @@ +# Page 1 Title diff --git a/tests/test_sphinx/test_sphinx_builds.py b/tests/test_sphinx/test_sphinx_builds.py index 351f6b7d..93b0a48e 100644 --- a/tests/test_sphinx/test_sphinx_builds.py +++ b/tests/test_sphinx/test_sphinx_builds.py @@ -532,3 +532,28 @@ def test_fieldlist_extension( regress_html=True, regress_ext=f".sphinx{sphinx.version_info[0]}.html", ) + + +@pytest.mark.sphinx( + buildername="html", + srcdir=os.path.join(SOURCE_DIR, "toclist"), + freshenv=True, +) +def test_toclist_extension( + app, + status, + warning, + get_sphinx_app_doctree, +): + """test enabling the toclist extension.""" + app.build() + assert "build succeeded" in status.getvalue() # Build succeeded + warnings = warning.getvalue().strip() + assert warnings == "" + + get_sphinx_app_doctree( + app, + docname="index", + regress=True, + regress_ext=".xml", + ) diff --git a/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml b/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml new file mode 100644 index 00000000..12660f79 --- /dev/null +++ b/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml @@ -0,0 +1,10 @@ + +
+ + Title + <compound classes="toctree-wrapper"> + <toctree caption="True" entries="(None,\ 'page1') (None,\ 'https://example.com') ('title',\ 'https://example.com')" glob="False" hidden="False" includefiles="page1" includehidden="False" maxdepth="-1" numbered="0" parent="index" rawentries="title" titlesonly="False"> + <bullet_list bullet="-"> + <list_item> + <paragraph> + A normal list From d55cfa54b77f4d8446c27d4775db8efc5951d1a4 Mon Sep 17 00:00:00 2001 From: Chris Sewell <chrisj_sewell@hotmail.com> Date: Fri, 31 Dec 2021 18:57:57 +0100 Subject: [PATCH 2/3] add config options to test --- tests/test_sphinx/sourcedirs/toclist/conf.py | 2 ++ tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_sphinx/sourcedirs/toclist/conf.py b/tests/test_sphinx/sourcedirs/toclist/conf.py index 2611c19b..ee3b76a2 100644 --- a/tests/test_sphinx/sourcedirs/toclist/conf.py +++ b/tests/test_sphinx/sourcedirs/toclist/conf.py @@ -1,3 +1,5 @@ extensions = ["myst_parser"] exclude_patterns = ["_build"] myst_enable_extensions = ["toclist"] +myst_toclist_maxdepth = 2 +myst_toclist_numbered = True diff --git a/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml b/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml index 12660f79..4bc01fc6 100644 --- a/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml +++ b/tests/test_sphinx/test_sphinx_builds/test_toclist_extension.xml @@ -3,7 +3,7 @@ <title> Title <compound classes="toctree-wrapper"> - <toctree caption="True" entries="(None,\ 'page1') (None,\ 'https://example.com') ('title',\ 'https://example.com')" glob="False" hidden="False" includefiles="page1" includehidden="False" maxdepth="-1" numbered="0" parent="index" rawentries="title" titlesonly="False"> + <toctree caption="True" entries="(None,\ 'page1') (None,\ 'https://example.com') ('title',\ 'https://example.com')" glob="False" hidden="False" includefiles="page1" includehidden="False" maxdepth="2" numbered="999" parent="index" rawentries="title" titlesonly="False"> <bullet_list bullet="-"> <list_item> <paragraph> From 32194f2d45f351419b9e4dc7975c9a9d0aba6946 Mon Sep 17 00:00:00 2001 From: Chris Sewell <chrisj_sewell@hotmail.com> Date: Fri, 31 Dec 2021 19:10:06 +0100 Subject: [PATCH 3/3] Add extension to config reference doc --- docs/sphinx/reference.md | 1 + docs/syntax/optional.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/reference.md b/docs/sphinx/reference.md index 5d3235c8..970b659c 100644 --- a/docs/sphinx/reference.md +++ b/docs/sphinx/reference.md @@ -69,6 +69,7 @@ List of extensions: - "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 +- "toclist": specify sphinx `toctree` in a Markdown native manner, see the [toclist syntax](syntax/toclists) for details Math specific, when `"dollarmath"` activated, see the [Math syntax](syntax/math) for more details: diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md index d2fb8a9e..4f16486c 100644 --- a/docs/syntax/optional.md +++ b/docs/syntax/optional.md @@ -683,7 +683,7 @@ Send a message to a recipient Currently `sphinx.ext.autodoc` does not support MyST, see [](howto/autodoc). ::: -(syntax/toclist)= +(syntax/toclists)= ## Table of Contents Lists By adding `"toclist"` to `myst_enable_extensions` (in the sphinx `conf.py` [configuration file](https://www.sphinx-doc.org/en/master/usage/configuration.html)),