From 69e97aa8128951c66799f5f28699b23345fb7377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Mon, 23 Aug 2021 11:52:24 +0200 Subject: [PATCH] Add mermaid-inheritance Add pytest-check-links to ensure links are working --- .travis.yml | 6 +- README.rst | 54 ++++- docs/conf.py | 68 +++--- docs/index.rst | 2 +- setup.py | 69 +++--- sphinxcontrib/__init__.py | 2 +- sphinxcontrib/autoclassdiag.py | 14 +- sphinxcontrib/exceptions.py | 3 +- sphinxcontrib/mermaid.py | 347 +++++++++++++++------------ sphinxcontrib/mermaid_inheritance.py | 317 ++++++++++++++++++++++++ tests/conftest.py | 6 +- tests/roots/test-basic/conf.py | 4 +- tests/roots/test-markdown/conf.py | 8 +- tests/test_html.py | 46 ++-- 14 files changed, 693 insertions(+), 253 deletions(-) create mode 100644 sphinxcontrib/mermaid_inheritance.py diff --git a/.travis.yml b/.travis.yml index 299d06a..f1f0219 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,5 @@ python: sudo: false cache: pip install: - - pip install -r docs/requirements.txt - - pip install pytest - - pip install -e . -script: pytest \ No newline at end of file + - pip install -e .[dev] +script: pytest && pytest --check-links --check-links-ignore "https://travis-ci.com/*" \ No newline at end of file diff --git a/README.rst b/README.rst index da68ce8..55e8989 100644 --- a/README.rst +++ b/README.rst @@ -82,6 +82,52 @@ Or directly the module:: .. autoclasstree:: sphinx.util +And alternative to `autoclasstree` directive is `mermaid-inheritance`. That directive mimics exactly the +official `inheritance_diagram `_ +extension but uses mermaid JS instead of graphviz to include the inheritance diagrams. + +It adds this directive:: + + .. mermaid-inheritance:: + +This directive has one or more arguments, each giving a module or class name. Class names can be unqualified; +in that case they are taken to exist in the currently described module. + +For each given class, and each class in each given module, the base classes are determined. Then, from all classes +and their base classes, a graph is generated which is then rendered via the ``mermaid`` extension to a directed graph. + +This directive supports an option called ``parts`` that, if given, must be an integer, advising the directive +to keep that many dot-separated parts in the displayed names (from right to left). For example, ``parts=1`` +will only display class names, without the names of the modules that contain them. + +The directive also supports a ``private-bases`` flag option; if given, private base classes (those whose name +starts with ``_``) will be included. + +You can use ``caption`` option to give a caption to the diagram. + +It also supports a ``top-classes`` option which requires one or more class names separated by comma. If specified +inheritance traversal will stop at the specified class names. + +For example:: + + .. mermaid-inheritance:: sphinx.ext.inheritance_diagram.InheritanceDiagram + +.. mermaid-inheritance:: sphinx.ext.inheritance_diagram.InheritanceDiagram + +.. note:: + + ``.`` are replaced by ``_`` in class name because mermaidJS does not allow them. + +To stop the diagram at ``SphinxDirective`` and only display the class name:: + + .. mermaid-inheritance:: sphinx.ext.inheritance_diagram.InheritanceDiagram + :top-classes: sphinx.util.docutils.SphinxDirective + :parts: 1 + + +.. mermaid-inheritance:: sphinx.ext.inheritance_diagram.InheritanceDiagram + :top-classes: sphinx.util.docutils.SphinxDirective + :parts: 1 Installation ------------ @@ -96,7 +142,9 @@ Then add ``sphinxcontrib.mermaid`` in ``extensions`` list of your project's ``co extensions = [ ..., - 'sphinxcontrib.mermaid' + 'sphinxcontrib.mermaid', + # Optional if you want the inheritance graphs + 'sphinxcontrib.mermaid-inheritance', ] @@ -149,7 +197,7 @@ Config values ``mermaid_params`` - For individual parameters, a list of parameters can be added. Refer to ``_. + For individual parameters, a list of parameters can be added. Refer to ``_. Examples:: mermaid_params = ['--theme', 'forest', '--width', '600', '--backgroundColor', 'transparent'] @@ -159,7 +207,7 @@ Config values ``mermaid_sequence_config`` Allows overriding the sequence diagram configuration. It could be useful to increase the width between actors. It **needs to be a json file** - Check options in the `documentation `_ + Check options in the `documentation `_ ``mermaid_verbose`` diff --git a/docs/conf.py b/docs/conf.py index 23530be..1d19f26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,35 +31,33 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'sphinxcontrib.mermaid' -] +extensions = ["sphinxcontrib.mermaid", "sphinxcontrib.mermaid_inheritance"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'sphinxcontrib-mermaid' -copyright = '2017-2021, Martín Gaitán' -author = 'Martín Gaitán' +project = "sphinxcontrib-mermaid" +copyright = "2017-2021, Martín Gaitán" +author = "Martín Gaitán" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '' +version = "" # The full version, including alpha/beta/rc tags. -release = '' +release = "" # The language for content autogenerated by Sphinx. Refer to documentation @@ -72,10 +70,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -86,7 +84,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -97,16 +95,16 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] html_js_files = [ - 'js/custom.js', + "js/custom.js", ] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'sphinxcontrib-mermaiddoc' +htmlhelp_basename = "sphinxcontrib-mermaiddoc" # -- Options for LaTeX output --------------------------------------------- @@ -115,15 +113,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -133,8 +128,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'sphinxcontrib-mermaid.tex', 'sphinxcontrib-mermaid documentation', - 'Martín Gaitán', 'manual'), + ( + master_doc, + "sphinxcontrib-mermaid.tex", + "sphinxcontrib-mermaid documentation", + "Martín Gaitán", + "manual", + ), ] @@ -143,8 +143,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'sphinxcontrib-mermaid', 'sphinxcontrib-mermaid documentation', - [author], 1) + ( + master_doc, + "sphinxcontrib-mermaid", + "sphinxcontrib-mermaid documentation", + [author], + 1, + ) ] @@ -154,10 +159,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'sphinxcontrib-mermaid', 'sphinxcontrib-mermaid documentation', - author, 'sphinxcontrib-mermaid', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "sphinxcontrib-mermaid", + "sphinxcontrib-mermaid documentation", + author, + "sphinxcontrib-mermaid", + "One line description of project.", + "Miscellaneous", + ), ] - - - diff --git a/docs/index.rst b/docs/index.rst index 68ae754..fba5a1e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -.. Sphinxcontrib-mesmaid demo documentation master file, created by +.. Sphinxcontrib-mermaid demo documentation master file, created by sphinx-quickstart on Sun Apr 23 13:10:20 2017. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. diff --git a/setup.py b/setup.py index ce432c4..6198908 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,9 @@ import io from setuptools import setup, find_packages -readme = io.open('README.rst', encoding="utf-8").read() -changes = io.open('CHANGELOG.rst', encoding="utf-8").read() -version = '0.7.1' +readme = io.open("README.rst", encoding="utf-8").read() +changes = io.open("CHANGELOG.rst", encoding="utf-8").read() +version = "0.7.1" def long_description(): @@ -25,40 +25,53 @@ def remove_block(text, token, margin=0): readme_ = remove_block(readme, ".. mermaid::", margin=2) readme_ = remove_block(readme_, ".. autoclasstree::") readme_ = remove_block(readme_, ".. autoclasstree::") + readme_ = remove_block(readme_, ".. mermaid-inheritance::") + readme_ = remove_block(readme_, ".. mermaid-inheritance::") readme_ = remove_block(readme_, ".. versionchanged::") return "{}\n\n{}".format(readme_, changes) setup( - name='sphinxcontrib-mermaid', + name="sphinxcontrib-mermaid", version=version, - url='https://github.com/mgaitan/sphinxcontrib-mermaid', - download_url='https://pypi.python.org/pypi/sphinxcontrib-mermaid', - license='BSD', - author=u'Martín Gaitán', - author_email='gaitan@gmail.com', - description='Mermaid diagrams in yours Sphinx powered docs', + url="https://github.com/mgaitan/sphinxcontrib-mermaid", + download_url="https://pypi.python.org/pypi/sphinxcontrib-mermaid", + license="BSD", + author=u"Martín Gaitán", + author_email="gaitan@gmail.com", + description="Mermaid diagrams in yours Sphinx powered docs", long_description=long_description(), classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Documentation', - 'Topic :: Utilities', + "Development Status :: 4 - Beta", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Documentation", + "Topic :: Utilities", ], - platforms='any', + platforms="any", packages=find_packages(), include_package_data=True, - namespace_packages=['sphinxcontrib'], + namespace_packages=["sphinxcontrib"], + python_requires=">=3.6", + install_requires=["sphinx>=3.2.1"], + extras_require={ + "dev": [ + "black", + "pytest>=6.0", + "pytest-check-links>=0.5", + "myst-parser>=0.15.1", + ], + "doc": ["myst-parser>=0.15.1"], + }, ) diff --git a/sphinxcontrib/__init__.py b/sphinxcontrib/__init__.py index 35d34fc..39e5e7d 100644 --- a/sphinxcontrib/__init__.py +++ b/sphinxcontrib/__init__.py @@ -10,4 +10,4 @@ :license: BSD, see LICENSE for details. """ -__import__('pkg_resources').declare_namespace(__name__) +__import__("pkg_resources").declare_namespace(__name__) diff --git a/sphinxcontrib/autoclassdiag.py b/sphinxcontrib/autoclassdiag.py index 3b2526f..90927fd 100644 --- a/sphinxcontrib/autoclassdiag.py +++ b/sphinxcontrib/autoclassdiag.py @@ -21,7 +21,9 @@ def get_classes(*cls_or_modules, strict=False): elif inspect.ismodule(obj): for obj_ in obj.__dict__.values(): - if inspect.isclass(obj_) and (not strict or obj_.__module__.startswith(obj.__name__)): + if inspect.isclass(obj_) and ( + not strict or obj_.__module__.startswith(obj.__name__) + ): yield obj_ else: raise MermaidError("%s is not a class nor a module" % cls_or_module) @@ -33,7 +35,7 @@ def class_diagram(*cls_or_modules, full=False, strict=False, namespace=None): def get_tree(cls): for base in cls.__bases__: - if base.__name__ == 'object': + if base.__name__ == "object": continue if namespace and not base.__module__.startswith(namespace): continue @@ -45,14 +47,12 @@ def get_tree(cls): get_tree(cls) return "classDiagram\n" + "\n".join( - " %s <|-- %s" % (a, b) - for a, b in inheritances - ) + " %s <|-- %s" % (a, b) for a, b in inheritances + ) if __name__ == "__main__": - class A: pass @@ -72,5 +72,3 @@ class E(C1): pass print(class_diagram("__main__.D", "__main__.E", full=True)) - - diff --git a/sphinxcontrib/exceptions.py b/sphinxcontrib/exceptions.py index fdf7385..f40474c 100644 --- a/sphinxcontrib/exceptions.py +++ b/sphinxcontrib/exceptions.py @@ -1,4 +1,5 @@ from sphinx.errors import SphinxError + class MermaidError(SphinxError): - category = 'Mermaid error' + category = "Mermaid error" diff --git a/sphinxcontrib/mermaid.py b/sphinxcontrib/mermaid.py index d7eb959..d6a57f1 100644 --- a/sphinxcontrib/mermaid.py +++ b/sphinxcontrib/mermaid.py @@ -31,8 +31,8 @@ logger = logging.getLogger(__name__) -mapname_re = re.compile(r' {code} @@ -203,70 +228,79 @@ def _render_mm_html_raw(self, node, code, options, prefix='mermaid', {code} """ - self.body.append(tag_template.format(align=node.get('align'), code=self.encode(code))) + self.body.append( + tag_template.format(align=node.get("align"), code=self.encode(code)) + ) raise nodes.SkipNode -def render_mm_html(self, node, code, options, prefix='mermaid', - imgcls=None, alt=None): - - _fmt = self.builder.config.mermaid_output_format - if _fmt == 'raw': - return _render_mm_html_raw(self, node, code, options, prefix='mermaid', - imgcls=None, alt=None) +def render_mm_html(self, node, code, options, prefix="mermaid", imgcls=None, alt=None): + format = self.builder.config.mermaid_output_format + if format == "raw": + return _render_mm_html_raw( + self, node, code, options, prefix="mermaid", imgcls=None, alt=None + ) try: - if _fmt not in ('png', 'svg'): - raise MermaidError("mermaid_output_format must be one of 'raw', 'png', " - "'svg', but is %r" % _fmt) + if format not in ("png", "svg"): + raise MermaidError( + "mermaid_output_format must be one of 'raw', 'png', " + "'svg', but is %r" % format + ) - fname, outfn = render_mm(self, code, options, _fmt, prefix) + fname, outfn = render_mm(self, code, options, format, prefix) except MermaidError as exc: - logger.warning('mermaid code %r: ' % code + str(exc)) + logger.warning("mermaid code %r: " % code + str(exc)) raise nodes.SkipNode if fname is None: self.body.append(self.encode(code)) else: if alt is None: - alt = node.get('alt', self.encode(code).strip()) - imgcss = imgcls and 'class="%s"' % imgcls or '' - if _fmt == 'svg': - svgtag = ''' -

%s

\n''' % (fname, alt) + alt = node.get("alt", self.encode(code).strip()) + imgcss = imgcls and 'class="%s"' % imgcls or "" + if format == "svg": + svgtag = """ +

%s

\n""" % ( + fname, + alt, + ) self.body.append(svgtag) else: - if 'align' in node: - self.body.append('
' % - (node['align'], node['align'])) + if "align" in node: + self.body.append( + '
' % (node["align"], node["align"]) + ) - self.body.append('%s\n' % - (fname, alt, imgcss)) - if 'align' in node: - self.body.append('
\n') + self.body.append('%s\n' % (fname, alt, imgcss)) + if "align" in node: + self.body.append("
\n") raise nodes.SkipNode def html_visit_mermaid(self, node): - render_mm_html(self, node, node['code'], node['options']) + render_mm_html(self, node, node["code"], node["options"]) -def render_mm_latex(self, node, code, options, prefix='mermaid'): +def render_mm_latex(self, node, code, options, prefix="mermaid"): try: - fname, outfn = render_mm(self, code, options, 'pdf', prefix) + fname, outfn = render_mm(self, code, options, "pdf", prefix) except MermaidError as exc: - logger.warning('mm code %r: ' % code + str(exc)) + logger.warning("mm code %r: " % code + str(exc)) raise nodes.SkipNode - if self.builder.config.mermaid_pdfcrop != '': + if self.builder.config.mermaid_pdfcrop != "": mm_args = [self.builder.config.mermaid_pdfcrop, outfn] try: p = Popen(mm_args, stdout=PIPE, stdin=PIPE, stderr=PIPE) except OSError as err: - if err.errno != ENOENT: # No such file or directory + if err.errno != ENOENT: # No such file or directory raise - logger.warning('command %r cannot be run (needed to crop pdf), check the mermaid_cmd setting' % self.builder.config.mermaid_pdfcrop) + logger.warning( + "command %r cannot be run (needed to crop pdf), check the mermaid_cmd setting" + % self.builder.config.mermaid_pdfcrop + ) return None, None stdout, stderr = p.communicate() @@ -274,31 +308,38 @@ def render_mm_latex(self, node, code, options, prefix='mermaid'): logger.info(stdout) if p.returncode != 0: - raise MermaidError('PdfCrop exited with error:\n[stderr]\n%s\n' - '[stdout]\n%s' % (stderr, stdout)) + raise MermaidError( + "PdfCrop exited with error:\n[stderr]\n%s\n" + "[stdout]\n%s" % (stderr, stdout) + ) if not os.path.isfile(outfn): - raise MermaidError('PdfCrop did not produce an output file:\n[stderr]\n%s\n' - '[stdout]\n%s' % (stderr, stdout)) + raise MermaidError( + "PdfCrop did not produce an output file:\n[stderr]\n%s\n" + "[stdout]\n%s" % (stderr, stdout) + ) - fname = '{filename[0]}-crop{filename[1]}'.format(filename=os.path.splitext(fname)) + fname = "{filename[0]}-crop{filename[1]}".format( + filename=os.path.splitext(fname) + ) is_inline = self.is_inline(node) if is_inline: - para_separator = '' + para_separator = "" else: - para_separator = '\n' + para_separator = "\n" if fname is not None: post = None - if not is_inline and 'align' in node: - if node['align'] == 'left': - self.body.append('{') - post = '\\hspace*{\\fill}}' - elif node['align'] == 'right': - self.body.append('{\\hspace*{\\fill}') - post = '}' - self.body.append('%s\\sphinxincludegraphics{%s}%s' % - (para_separator, fname, para_separator)) + if not is_inline and "align" in node: + if node["align"] == "left": + self.body.append("{") + post = "\\hspace*{\\fill}}" + elif node["align"] == "right": + self.body.append("{\\hspace*{\\fill}") + post = "}" + self.body.append( + "%s\\sphinxincludegraphics{%s}%s" % (para_separator, fname, para_separator) + ) if post: self.body.append(post) @@ -306,74 +347,76 @@ def render_mm_latex(self, node, code, options, prefix='mermaid'): def latex_visit_mermaid(self, node): - render_mm_latex(self, node, node['code'], node['options']) + render_mm_latex(self, node, node["code"], node["options"]) -def render_mm_texinfo(self, node, code, options, prefix='mermaid'): +def render_mm_texinfo(self, node, code, options, prefix="mermaid"): try: - fname, outfn = render_mm(self, code, options, 'png', prefix) + fname, outfn = render_mm(self, code, options, "png", prefix) except MermaidError as exc: - logger.warning('mm code %r: ' % code + str(exc)) + logger.warning("mm code %r: " % code + str(exc)) raise nodes.SkipNode if fname is not None: - self.body.append('@image{%s,,,[mermaid],png}\n' % fname[:-4]) + self.body.append("@image{%s,,,[mermaid],png}\n" % fname[:-4]) raise nodes.SkipNode def texinfo_visit_mermaid(self, node): - render_mm_texinfo(self, node, node['code'], node['options']) + render_mm_texinfo(self, node, node["code"], node["options"]) def text_visit_mermaid(self, node): - if 'alt' in node.attributes: - self.add_text(_('[graph: %s]') % node['alt']) + if "alt" in node.attributes: + self.add_text(_("[graph: %s]") % node["alt"]) else: - self.add_text(_('[graph]')) + self.add_text(_("[graph]")) raise nodes.SkipNode def man_visit_mermaid(self, node): - if 'alt' in node.attributes: - self.body.append(_('[graph: %s]') % node['alt']) + if "alt" in node.attributes: + self.body.append(_("[graph: %s]") % node["alt"]) else: - self.body.append(_('[graph]')) + self.body.append(_("[graph]")) raise nodes.SkipNode -def install_js(app, *args): - # add required javascript - if not app.config.mermaid_version: - _mermaid_js_url = None # asummed is local - elif app.config.mermaid_version == "latest": - _mermaid_js_url = f"https://unpkg.com/mermaid/dist/mermaid.min.js" - else: - _mermaid_js_url = f"https://unpkg.com/mermaid@{app.config.mermaid_version}/dist/mermaid.min.js" - if _mermaid_js_url: - app.add_js_file(_mermaid_js_url) +def config_inited(app, config): + if config.mermaid_version: + version = ( + "" if config.mermaid_version == "latest" else f"@{config.mermaid_version}" + ) + mermaid_js_url = f"https://unpkg.com/mermaid{version}/dist/mermaid.min.js" + app.add_js_file(mermaid_js_url) - if app.config.mermaid_init_js: - app.add_js_file(None, body=app.config.mermaid_init_js) + if config.mermaid_init_js: + app.add_js_file(None, body=config.mermaid_init_js) def setup(app): - app.add_node(mermaid, - html=(html_visit_mermaid, None), - latex=(latex_visit_mermaid, None), - texinfo=(texinfo_visit_mermaid, None), - text=(text_visit_mermaid, None), - man=(man_visit_mermaid, None)) - app.add_directive('mermaid', Mermaid) - app.add_directive('autoclasstree', MermaidClassDiagram) - - app.add_config_value('mermaid_cmd', 'mmdc', 'html') - app.add_config_value('mermaid_cmd_shell', 'False', 'html') - app.add_config_value('mermaid_pdfcrop', '', 'html') - app.add_config_value('mermaid_output_format', 'raw', 'html') - app.add_config_value('mermaid_params', list(), 'html') - app.add_config_value('mermaid_verbose', False, 'html') - app.add_config_value('mermaid_sequence_config', False, 'html') - app.add_config_value('mermaid_version', 'latest', 'html') - app.add_config_value('mermaid_init_js', "mermaid.initialize({startOnLoad:true});", 'html') - app.connect('html-page-context', install_js) - - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + app.add_node( + mermaid, + html=(html_visit_mermaid, None), + latex=(latex_visit_mermaid, None), + texinfo=(texinfo_visit_mermaid, None), + text=(text_visit_mermaid, None), + man=(man_visit_mermaid, None), + ) + app.add_directive("mermaid", Mermaid) + app.add_directive("autoclasstree", MermaidClassDiagram) + + app.add_config_value("mermaid_cmd", "mmdc", "html") + app.add_config_value("mermaid_cmd_shell", "False", "html") + app.add_config_value("mermaid_pdfcrop", "", "html") + app.add_config_value("mermaid_output_format", "raw", "html") + app.add_config_value("mermaid_params", list(), "html") + app.add_config_value("mermaid_verbose", False, "html") + app.add_config_value("mermaid_sequence_config", False, "html") + app.add_config_value("mermaid_version", "latest", "html") + app.add_config_value( + "mermaid_init_js", "mermaid.initialize({startOnLoad:true});", "html" + ) + + app.connect("config-inited", config_inited) + + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/sphinxcontrib/mermaid_inheritance.py b/sphinxcontrib/mermaid_inheritance.py new file mode 100644 index 0000000..9e6ba39 --- /dev/null +++ b/sphinxcontrib/mermaid_inheritance.py @@ -0,0 +1,317 @@ +r""" + mermaid_inheritance + ~~~~~~~~~~~~~~~~~~~ + + Modified from sphinx.ext.inheritance_diagram + + Defines a docutils directive for inserting inheritance diagrams. + Provide the directive with one or more classes or modules (separated + by whitespace). For modules, all of the classes in that module will + be used. + Example:: + Given the following classes: + class A: pass + class B(A): pass + class C(A): pass + class D(B, C): pass + class E(B): pass + .. inheritance-diagram: D E + Produces a graph like the following: + A + / \ + B C + / \ / + E D + The graph is inserted as a PNG+image map into HTML and a PDF in + LaTeX. + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from typing import Any, Dict, Iterable, List, cast + +import sphinx +from docutils import nodes +from docutils.nodes import Node +from docutils.parsers.rst import directives +from sphinx import addnodes +from sphinx.application import Sphinx +from sphinx.environment import BuildEnvironment +from sphinx.ext.inheritance_diagram import ( + InheritanceDiagram, + InheritanceException, + InheritanceGraph, + figure_wrapper, + get_graph_hash, + inheritance_diagram, + skip, +) +from sphinx.util.docutils import SphinxDirective +from sphinx.writers.html import HTMLTranslator +from sphinx.writers.latex import LaTeXTranslator +from sphinx.writers.texinfo import TexinfoTranslator + +from sphinxcontrib.mermaid import render_mm_html, render_mm_latex, render_mm_texinfo + + +class MermaidGraph(InheritanceGraph): + """ + Given a list of classes, determines the set of classes that they inherit + from all the way to the root "object", and then is able to generate a + mermaid graph from them. + """ + + # These are the default attrs + default_graph_attrs = {} + # 'rankdir': 'LR', + # 'size': '"8.0, 12.0"', + # 'bgcolor': 'transparent', + # } + default_node_attrs = {} + # 'shape': 'box', + # 'fontsize': 10, + # 'height': 0.25, + # 'fontname': '"Vera Sans, DejaVu Sans, Liberation Sans, ' + # 'Arial, Helvetica, sans"', + # 'style': '"setlinewidth(0.5),filled"', + # 'fillcolor': 'white', + # } + default_edge_attrs = {} + # 'arrowsize': 0.5, + # 'style': '"setlinewidth(0.5)"', + # } + + def _format_node_attrs(self, attrs: Dict) -> str: + # return ','.join(['%s=%s' % x for x in sorted(attrs.items())]) + return "" + + def _format_graph_attrs(self, attrs: Dict) -> str: + # return ''.join(['%s=%s;\n' % x for x in sorted(attrs.items())]) + return "" + + def generate_dot( + self, + name: str, + urls: Dict = {}, + env: BuildEnvironment = None, + graph_attrs: Dict = {}, + node_attrs: Dict = {}, + edge_attrs: Dict = {}, + ) -> str: + """Generate a mermaid graph from the classes that were passed in + to __init__. + *name* is the name of the graph. + *urls* is a dictionary mapping class names to HTTP URLs. + *graph_attrs*, *node_attrs*, *edge_attrs* are dictionaries containing + key/value pairs to pass on as graphviz properties. + """ + # g_attrs = self.default_graph_attrs.copy() + # n_attrs = self.default_node_attrs.copy() + # e_attrs = self.default_edge_attrs.copy() + # g_attrs.update(graph_attrs) + # n_attrs.update(node_attrs) + # e_attrs.update(edge_attrs) + # if env: + # g_attrs.update(env.config.inheritance_graph_attrs) + # n_attrs.update(env.config.inheritance_node_attrs) + # e_attrs.update(env.config.inheritance_edge_attrs) + + res = [] # type: List[str] + + res.append("classDiagram\n") + for name, fullname, bases, tooltip in sorted(self.class_info): + # mermaidJS does not support '.' in class name + mermaidName = name.replace(".", "_") + # Write the node + res.append(" class {!s}\n".format(mermaidName)) + if fullname in urls: + res.append( + ' link {!s} "./{!s}" {!s}\n'.format( + mermaidName, urls[fullname], tooltip or '"{}"'.format(name) + ) + ) + + # Write the edges + for base_name in bases: + res.append( + " {!s} <|-- {!s}\n".format( + base_name.replace(".", "_"), mermaidName + ) + ) + + return "".join(res) + + +class mermaid_inheritance(inheritance_diagram): + """ + A docutils node to use as a placeholder for the inheritance diagram. + """ + + pass + + +class MermaidDiagram(InheritanceDiagram): + """ + Run when the mermaid_inheritance directive is first encountered. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = { + "parts": int, + "private-bases": directives.flag, + "caption": directives.unchanged, + "top-classes": directives.unchanged_required, + } + + def run(self) -> List[Node]: + node = mermaid_inheritance() + node.document = self.state.document + class_names = self.arguments[0].split() + class_role = self.env.get_domain("py").role("class") + # Store the original content for use as a hash + node["parts"] = self.options.get("parts", 0) + node["content"] = ", ".join(class_names) + node["top-classes"] = [] + for cls in self.options.get("top-classes", "").split(","): + cls = cls.strip() + if cls: + node["top-classes"].append(cls) + + # Create a graph starting with the list of classes + try: + graph = MermaidGraph( + class_names, + self.env.ref_context.get("py:module"), + parts=node["parts"], + private_bases="private-bases" in self.options, + aliases=self.config.inheritance_alias, + top_classes=node["top-classes"], + ) + except InheritanceException as err: + return [node.document.reporter.warning(err, line=self.lineno)] + + # Create xref nodes for each target of the graph's image map and + # add them to the doc tree so that Sphinx can resolve the + # references to real URLs later. These nodes will eventually be + # removed from the doctree after we're done with them. + for name in graph.get_all_class_names(): + refnodes, x = class_role( # type: ignore + "class", ":class:`%s`" % name, name, 0, self.state + ) # type: ignore + node.extend(refnodes) + # Store the graph object so we can use it to generate the + # dot file later + node["graph"] = graph + + if "caption" not in self.options: + self.add_name(node) + return [node] + else: + figure = figure_wrapper(self, node, self.options["caption"]) + self.add_name(figure) + return [figure] + + +def html_visit_mermaid_inheritance( + self: HTMLTranslator, node: inheritance_diagram +) -> None: + """ + Output the graph for HTML. This will insert a PNG with clickable + image map. + """ + graph = node["graph"] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + + # Create a mapping from fully-qualified class names to URLs. + mermaid_output_format = self.builder.env.config.mermaid_output_format.upper() + current_filename = self.builder.current_docname + self.builder.out_suffix + urls = {} + pending_xrefs = cast(Iterable[addnodes.pending_xref], node) + for child in pending_xrefs: + if child.get("refuri") is not None: + if mermaid_output_format == "SVG": + urls[child["reftitle"]] = "../" + child.get("refuri") + else: + urls[child["reftitle"]] = child.get("refuri") + elif child.get("refid") is not None: + if mermaid_output_format == "SVG": + urls[child["reftitle"]] = ( + "../" + current_filename + "#" + child.get("refid") + ) + else: + urls[child["reftitle"]] = "#" + child.get("refid") + dotcode = graph.generate_dot(name, urls, env=self.builder.env) + render_mm_html( + self, + node, + dotcode, + {}, + "inheritance", + "inheritance", + alt="Inheritance diagram of " + node["content"], + ) + raise nodes.SkipNode + + +def latex_visit_mermaid_inheritance( + self: LaTeXTranslator, node: inheritance_diagram +) -> None: + """ + Output the graph for LaTeX. This will insert a PDF. + """ + graph = node["graph"] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + + dotcode = graph.generate_dot( + name, + env=self.builder.env, + ) + # graph_attrs={'size': '"6.0,6.0"'}) + render_mm_latex(self, node, dotcode, {}, "inheritance") + raise nodes.SkipNode + + +def texinfo_visit_mermaid_inheritance( + self: TexinfoTranslator, node: inheritance_diagram +) -> None: + """ + Output the graph for Texinfo. This will insert a PNG. + """ + graph = node["graph"] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + + dotcode = graph.generate_dot( + name, + env=self.builder.env, + ) + # graph_attrs={'size': '"6.0,6.0"'}) + render_mm_texinfo(self, node, dotcode, {}, "inheritance") + raise nodes.SkipNode + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.setup_extension("sphinxcontrib.mermaid") + app.add_node( + mermaid_inheritance, + latex=(latex_visit_mermaid_inheritance, None), + html=(html_visit_mermaid_inheritance, None), + text=(skip, None), + man=(skip, None), + texinfo=(texinfo_visit_mermaid_inheritance, None), + ) + app.add_directive("mermaid-inheritance", MermaidDiagram) + # app.add_config_value('mermaid_inheritance_graph_attrs', {}, False) + # app.add_config_value('mermaid_inheritance_node_attrs', {}, False) + # app.add_config_value('mermaid_inheritance_edge_attrs', {}, False) + app.add_config_value("inheritance_alias", {}, False) + + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/tests/conftest.py b/tests/conftest.py index 3c896fd..c9f1be3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,9 @@ import pytest from sphinx.testing.path import path -pytest_plugins = 'sphinx.testing.fixtures' +pytest_plugins = "sphinx.testing.fixtures" -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def rootdir(): - return path(__file__).parent.abspath() / 'roots' \ No newline at end of file + return path(__file__).parent.abspath() / "roots" diff --git a/tests/roots/test-basic/conf.py b/tests/roots/test-basic/conf.py index cadab24..85008e1 100644 --- a/tests/roots/test-basic/conf.py +++ b/tests/roots/test-basic/conf.py @@ -1,2 +1,2 @@ -extensions = ['sphinxcontrib.mermaid'] -exclude_patterns = ['_build'] \ No newline at end of file +extensions = ["sphinxcontrib.mermaid"] +exclude_patterns = ["_build"] diff --git a/tests/roots/test-markdown/conf.py b/tests/roots/test-markdown/conf.py index ca2ffe5..38e59c0 100644 --- a/tests/roots/test-markdown/conf.py +++ b/tests/roots/test-markdown/conf.py @@ -1,6 +1,4 @@ -extensions = ['sphinxcontrib.mermaid', 'myst_parser'] -exclude_patterns = ['_build'] +extensions = ["sphinxcontrib.mermaid", "myst_parser"] +exclude_patterns = ["_build"] -source_suffix = { - '.md': 'markdown' -} \ No newline at end of file +source_suffix = {".md": "markdown"} diff --git a/tests/test_html.py b/tests/test_html.py index f574b8b..cb42010 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,5 +1,6 @@ import pytest + @pytest.fixture def build_all(app): app.builder.build_all() @@ -8,47 +9,62 @@ def build_all(app): @pytest.fixture def index(app, build_all): # normalize script tag for compat to Sphinx<4 - return (app.outdir / 'index.html').read_text().replace("' in index + assert ( + '' in index + ) assert "" in index - assert """
+ assert ( + """
sequenceDiagram participant Alice participant Bob Alice->John: Hello John, how are you? -
""" in index +
""" + in index + ) -@pytest.mark.sphinx('html', testroot="basic", confoverrides={'mermaid_version': '8.3'}) +@pytest.mark.sphinx("html", testroot="basic", confoverrides={"mermaid_version": "8.3"}) def test_conf_mermaid_version(app, index): assert app.config.mermaid_version == "8.3" - assert '' in index + assert ( + '' + in index + ) -@pytest.mark.sphinx('html', testroot="basic", confoverrides={'mermaid_version': None}) +@pytest.mark.sphinx("html", testroot="basic", confoverrides={"mermaid_version": None}) def test_conf_mermaid_no_version(app, index): # requires local mermaid - assert 'mermaid.min.js' not in index + assert "mermaid.min.js" not in index -@pytest.mark.sphinx('html', testroot="basic", confoverrides={'mermaid_init_js': "custom script;"}) +@pytest.mark.sphinx( + "html", testroot="basic", confoverrides={"mermaid_init_js": "custom script;"} +) def test_mermaid_init_js(index): assert "" not in index - assert '' in index + assert "" in index -@pytest.mark.sphinx('html', testroot="markdown") +@pytest.mark.sphinx("html", testroot="markdown") def test_html_raw_from_markdown(index): - assert '' in index + assert ( + '' in index + ) assert "" in index - assert """ + assert ( + """
sequenceDiagram participant Alice participant Bob Alice->John: Hello John, how are you? -
""" in index \ No newline at end of file + """ + in index + )