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