add sphinx renderer
diff --git a/markdown_it/myst/renderer.py b/markdown_it/myst/doc_renderer.py similarity index 89% rename from markdown_it/myst/renderer.py rename to markdown_it/myst/doc_renderer.py index 0da8548..0201782 100644 --- a/markdown_it/myst/renderer.py +++ b/markdown_it/myst/doc_renderer.py
@@ -1,7 +1,10 @@ """NOTE: this will eventually be moved out of core""" from contextlib import contextmanager +import inspect import json +from os.path import splitext from typing import List, Optional +from urllib.parse import urlparse import yaml @@ -36,7 +39,7 @@ return new_document(source_path, settings=settings) -class DocRenderer: +class DocutilsRenderer: __output__ = "docutils" def __init__( @@ -50,7 +53,7 @@ self.options = options or {} self.rules = { k: v - for k, v in self.__class__.__dict__.items() + for k, v in inspect.getmembers(self, predicate=inspect.ismethod) if k.startswith("render_") and k != "render_children" } self.document = document or make_document() @@ -93,7 +96,7 @@ for i, token in enumerate(tokens): # skip hidden? if f"render_{token.type}" in self.rules: - self.rules[f"render_{token.type}"](self, token) + self.rules[f"render_{token.type}"](token) else: print(f"no render method for: {token.type}") @@ -148,7 +151,7 @@ for i, token in enumerate(tokens): # skip hidden? if f"render_{token.type}" in self.rules: - self.rules[f"render_{token.type}"](self, token) + self.rules[f"render_{token.type}"](token) else: print(f"no render method for: {token.type}") @@ -165,7 +168,7 @@ def render_children(self, token): for i, child in enumerate(token.children or []): if f"render_{child.type}" in self.rules: - self.rules[f"render_{child.type}"](self, child) + self.rules[f"render_{child.type}"](child) else: print(f"no render method for: {child.type}") @@ -330,10 +333,57 @@ self.current_node = section def render_link_open(self, token): - # TODO I think this is maybe already handled at this point? - # refuri = escape_url(token.target) - # TODO identify cross-references - refuri = target = token.attrGet("href") + if token.markup == "autolink": + return self.render_autolink(token) + + ref_node = nodes.reference() + self.add_line_and_source_path(ref_node, token) + # Check destination is supported for cross-linking and remove extension + # TODO escape urls? + destination = token.attrGet("href") + title = token.attrGet("title") + _, ext = splitext(destination) + # TODO check for other supported extensions, such as those specified in + # the Sphinx conf.py file but how to access this information? + # TODO this should probably only remove the extension for local paths, + # i.e. not uri's starting with http or other external prefix. + + # if ext.replace('.', '') in self.supported: + # destination = destination.replace(ext, '') + ref_node["refuri"] = destination + print(token) + if title: + ref_node["title"] = title + next_node = ref_node + + url_check = urlparse(destination) + # If there's not a url scheme (e.g. 'https' for 'https:...' links), + # or there is a scheme but it's not in the list of known_url_schemes, + # then assume it's a cross-reference + known_url_schemes = self.config.get("known_url_schemes", None) + if known_url_schemes: + scheme_known = url_check.scheme in known_url_schemes + else: + scheme_known = bool(url_check.scheme) + + if not url_check.fragment and not scheme_known: + self.handle_cross_reference(token, destination) + else: + self.current_node.append(next_node) + with self.current_node_context(ref_node): + self.render_children(token) + + def handle_cross_reference(self, token, destination): + # TODO use the docutils error reporting mechanisms, rather than raising + if not self.config.get("ignore_missing_refs", False): + raise NotImplementedError( + "reference not found in current document: {} (lines: {})".format( + destination, token.map + ) + ) + + def render_autolink(self, token): + refuri = target = escapeHtml(token.attrGet("href")) ref_node = nodes.reference(target, target, refuri=refuri) self.add_line_and_source_path(ref_node, token) self.current_node.append(ref_node)
diff --git a/markdown_it/myst/mocking.py b/markdown_it/myst/mocking.py index ef6d8d3..af045ed 100644 --- a/markdown_it/myst/mocking.py +++ b/markdown_it/myst/mocking.py
@@ -111,9 +111,9 @@ # so that the nested parse does not effect the current renderer, # but we use the same env, so that link references, etc # are added to the global parse. - from .renderer import DocRenderer + from .doc_renderer import DocutilsRenderer - nested_renderer = DocRenderer( + nested_renderer = DocutilsRenderer( self._renderer.md, document=self.document, current_node=paragraph ) nested_renderer.run_render(tokens, self._renderer.env, ouput_footnotes=False)
diff --git a/markdown_it/myst/sphinx_renderer.py b/markdown_it/myst/sphinx_renderer.py new file mode 100644 index 0000000..a16da70 --- /dev/null +++ b/markdown_it/myst/sphinx_renderer.py
@@ -0,0 +1,161 @@ +import copy +from urllib.parse import unquote + +from docutils import nodes +from docutils.parsers.rst import directives, roles + +from .doc_renderer import DocutilsRenderer + + +class SphinxRenderer(DocutilsRenderer): + """A mistletoe renderer to populate (in-place) a `docutils.document` AST. + + This is sub-class of `DocutilsRenderer` that handles sphinx cross-referencing. + """ + + def __init__(self, *args, **kwargs): + """Initialise SphinxRenderer + + :param load_sphinx_env: load a basic sphinx environment, + when using the renderer as a context manager outside if `sphinx-build` + :param sphinx_conf: a dictionary representation of the sphinx `conf.py` + :param sphinx_srcdir: a path to a source directory + (for example, can be used for `include` statements) + + To use this renderer in a 'standalone' fashion:: + + from myst_parser.block_tokens import Document + + with SphinxRenderer(load_sphinx_env=True, sphinx_conf={}) as renderer: + renderer.render(Document.read("source text")) + + """ + self.load_sphinx_env = kwargs.pop("load_sphinx_env", False) + self.sphinx_conf = kwargs.pop("sphinx_conf", None) + self.sphinx_srcdir = kwargs.pop("sphinx_srcdir", None) + super().__init__(*args, **kwargs) + + def handle_cross_reference(self, token, destination): + from sphinx import addnodes + + wrap_node = addnodes.pending_xref( + reftarget=unquote(destination), + reftype="any", + refdomain=None, # Added to enable cross-linking + refexplicit=len(token.children) > 0, + refwarn=True, + ) + 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) + text_node = nodes.literal("", "", classes=["xref", "any"]) + wrap_node.append(text_node) + with self.current_node_context(text_node): + self.render_children(token) + + def mock_sphinx_env(self, configuration=None, sourcedir=None): + """Create a minimimal Sphinx environment; + loading sphinx roles, directives, etc. + """ + from sphinx.application import builtin_extensions, Sphinx + from sphinx.config import Config + from sphinx.environment import BuildEnvironment + from sphinx.events import EventManager + from sphinx.project import Project + from sphinx.registry import SphinxComponentRegistry + from sphinx.util.tags import Tags + + class MockSphinx(Sphinx): + """Minimal sphinx init to load roles and directives.""" + + def __init__(self, confoverrides=None, srcdir=None): + self.extensions = {} + self.registry = SphinxComponentRegistry() + self.html_themes = {} + self.events = EventManager(self) + self.tags = Tags(None) + 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 = None + self.srcdir = srcdir + self.confdir = None + self.outdir = None + self.project = Project(srcdir=srcdir, source_suffix=".md") + self.project.docnames = ["mock_docname"] + self.env = BuildEnvironment() + self.env.setup(self) + self.env.temp_data["docname"] = "mock_docname" + self.builder = None + + if not confoverrides: + 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) + import tempfile + + with tempfile.TemporaryDirectory() as tempdir: + # creating a builder attempts to make the doctreedir + self.doctreedir = tempdir + self.builder = self.create_builder(buildername) + self.doctreedir = None + + app = MockSphinx(confoverrides=configuration, srcdir=sourcedir) + self.document.settings.env = app.env + return app + + def __enter__(self): + """If `load_sphinx_env=True`, we set up an environment, + to parse sphinx roles/directives, outside of a `sphinx-build`. + + This primarily copies the code in `sphinx.util.docutils.docutils_namespace` + and `sphinx.util.docutils.sphinx_domains`. + """ + if not self.load_sphinx_env: + return super().__enter__() + + # store currently loaded roles/directives, so we can revert on exit + self._directives = copy.copy(directives._directives) + self._roles = copy.copy(roles._roles) + # Monkey-patch directive and role dispatch, + # so that sphinx domain-specific markup takes precedence. + self._env = self.mock_sphinx_env( + configuration=self.sphinx_conf, sourcedir=self.sphinx_srcdir + ).env + from sphinx.util.docutils import sphinx_domains + + self._sphinx_domains = sphinx_domains(self._env) + self._sphinx_domains.enable() + + return super().__enter__() + + def __exit__(self, exception_type, exception_val, traceback): + if not self.load_sphinx_env: + return super().__exit__(exception_type, exception_val, traceback) + # revert loaded roles/directives + directives._directives = self._directives + roles._roles = self._roles + self._directives = None + self._roles = None + # unregister nodes (see `sphinx.util.docutils.docutils_namespace`) + from sphinx.util.docutils import additional_nodes, unregister_node + + for node in list(additional_nodes): + unregister_node(node) + additional_nodes.discard(node) + # revert directive/role function (see `sphinx.util.docutils.sphinx_domains`) + self._sphinx_domains.disable() + self._sphinx_domains = None + self._env = None + return super().__exit__(exception_type, exception_val, traceback)
diff --git a/try_running_renderer.py b/try_running_renderer.py index 6c0a99e..8a92ed2 100644 --- a/try_running_renderer.py +++ b/try_running_renderer.py
@@ -5,7 +5,7 @@ from markdown_it.extensions.myst_role import myst_role_plugin from markdown_it.extensions.texmath import texmath_plugin from markdown_it.extensions.footnote import footnote_plugin -from markdown_it.myst.renderer import DocRenderer +from markdown_it.myst.sphinx_renderer import SphinxRenderer md = ( MarkdownIt() @@ -52,7 +52,7 @@ [a][b] -[b]: s +[b]: s "a" <div>A</div> @@ -88,11 +88,13 @@ [^dfg] [^dfg]: xyc ```` +[dvs]: a +[dvs] """, env=env, ) -doc = DocRenderer(md) +doc = SphinxRenderer(md) doc.run_render(tokens, env) print(len(env["foot_refs"])) print(doc.document.pformat())