Source code for pyquickhelper.sphinxext.sphinx_gdot_extension

# -*- coding: utf-8 -*-
"""
Defines a sphinx extension to show :epkg:`DOT` graph
with :epkg:`viz.js` or :epkg:`graphviz`.

.. versionadded:: 1.9


:githublink:`%|py|9`
"""
import os
import logging
import shutil
from docutils import nodes
from docutils.parsers.rst import directives
import sphinx
from docutils.parsers.rst import Directive
from .sphinxext_helper import get_env_state_info
from .sphinx_runpython_extension import run_python_script


[docs]class gdot_node(nodes.admonition): """ defines ``gdot`` node. :githublink:`%|py|23` """ pass
[docs]class GDotDirective(Directive): """ A ``gdot`` node displays a :epkg:`DOT` graph. The build choose :epkg:`SVG` for :epkg:`HTML` format and image for other format unless it is specified. * *format*: SVG or HTML * *script*: boolean or a string to indicate than the standard output should only be considered after this substring * *url*: url to :epkg:`viz.js`, only if format *SVG* is selected Example:: .. gdot:: digraph foo { "bar" -> "baz"; } Which gives: .. gdot:: digraph foo { "bar" -> "baz"; } The directive also accepts scripts producing dot graphs on the standard output. Option *script* must be specified. This extension loads `sphinx.ext.graphviz <https://www.sphinx-doc.org/en/master/usage/extensions/graphviz.html>`_ if not added to the list of extensions: Example:: .. gdot:: :format: png digraph foo { "bar" -> "baz"; } .. gdot:: :format: png digraph foo { "bar" -> "baz"; } :githublink:`%|py|75` """ node_class = gdot_node has_content = True required_arguments = 0 optional_arguments = 0 final_argument_whitespace = False option_spec = { 'format': directives.unchanged, 'script': directives.unchanged, 'url': directives.unchanged, } _default_url = "http://www.xavierdupre.fr/js/vizjs/viz.js"
[docs] def run(self): """ Builds the collapse text. :githublink:`%|py|92` """ # retrieves the parameters if 'format' in self.options: format = self.options['format'] else: format = '?' url = self.options.get('url', 'local') if url == 'local': try: import jyquickhelper path = os.path.join(os.path.dirname( jyquickhelper.__file__), "js", "vizjs", "viz.js") if not os.path.exists(path): raise ImportError( "jyquickelper needs to be updated to get viz.js.") url = 'local' except ImportError: url = GDotDirective._default_url logger = logging.getLogger("gdot") logger.warning("[gdot] jyquickhelper not installed, falling back to " "'{}'".format(url)) info = get_env_state_info(self) docname = info['docname'] if url == 'local': if docname is None or 'HERE' not in info: url = GDotDirective._default_url logger = logging.getLogger("gdot") logger.warning("[gdot] docname is none, falling back to " "'{}'".format(url)) else: spl = docname.split("/") sp = ['..'] * (len(spl) - 1) + ['_static', 'viz.js'] url = "/".join(sp) if 'script' in self.options: script = self.options['script'] if script in (0, "0", "False", 'false'): script = None elif script in (1, "1", "True", 'true', ''): script = '' elif len(script) == 0: raise RuntimeError("script should be a string to indicate" " the beginning of DOT graph.") else: script = False # executes script if any content = "\n".join(self.content) if script or script == '': stdout, stderr, _ = run_python_script(content) if stderr: raise RuntimeError( "A graph cannot be draw due to {}".format(stderr)) content = stdout if script: spl = content.split(script) if len(spl) > 2: raise RuntimeError("'{}' indicates the beginning of the graph " "but there are many in\n{}".format(script, content)) content = spl[-1] node = gdot_node(format=format, code=content, url=url, options={'docname': docname}) return [node]
[docs]def visit_gdot_node_rst(self, node): """ visit collapse_node :githublink:`%|py|162` """ self.new_state(0) self.add_text('.. gdot::' + self.nl) if node['format'] != '?': self.add_text(' :format: ' + node['format'] + self.nl) if node['url']: self.add_text(' :url: ' + node['url'] + self.nl) self.new_state(self.indent) for row in node['code'].split('\n'): self.add_text(row + self.nl)
[docs]def depart_gdot_node_rst(self, node): """ depart collapse_node :githublink:`%|py|177` """ self.end_state() self.end_state(wrap=False)
[docs]def visit_gdot_node_html_svg(self, node): """ visit collapse_node :githublink:`%|py|185` """ def process(text): text = text.replace("\\", "\\\\") text = text.replace("\n", "\\n") text = text.replace('"', '\\"') return text nid = str(id(node)) content = """ <div id="gdot-{0}-cont"><div id="gdot-{0}" style="width:100%;height:100%;"></div> """.format(nid) script = """ require(['__URL__'], function() { var svgGraph = Viz("__DOT__"); document.getElementById('gdot-__ID__').innerHTML = svgGraph; }); """.replace('__ID__', nid).replace('__DOT__', process(node['code'])).replace( "__URL__", node['url']) self.body.append(content) self.body.append("<script>{0}{1}{0}</script>{0}".format("\n", script))
[docs]def depart_gdot_node_html_svg(self, node): """ depart collapse_node :githublink:`%|py|211` """ self.body.append("</div>")
[docs]def visit_gdot_node_html(self, node): """ visit collapse_node, the function switches between `graphviz.py <https://github.com/sphinx-doc/sphinx/blob/ master/sphinx/ext/graphviz.py>`_ and the :epkg:`SVG` format. :githublink:`%|py|220` """ if node['format'].lower() == 'png': from sphinx.ext.graphviz import html_visit_graphviz return html_visit_graphviz(self, node) if node['format'].lower() in ('?', 'svg'): return visit_gdot_node_html_svg(self, node) raise RuntimeError( "Unexpected format for graphviz '{}'.".format(node['format']))
[docs]def depart_gdot_node_html(self, node): """ depart collapse_node :githublink:`%|py|233` """ if node['format'] == 'png': return None return depart_gdot_node_html_svg(self, node)
def copy_js_files(app): try: import jyquickhelper local = True except ImportError: local = False logger = logging.getLogger("gdot") if local: path = os.path.join(os.path.dirname( jyquickhelper.__file__), "js", "vizjs", "viz.js") if os.path.exists(path): # We copy the file to static path. dest = app.config.html_static_path if isinstance(dest, list) and len(dest) > 0: dest = dest[0] else: dest = None srcdir = app.builder.srcdir if "IMPOSSIBLE:TOFIND" not in srcdir: if not os.path.exists(srcdir): raise FileNotFoundError( "Source file is wrong '{}'.".format(srcdir)) if dest is not None: destf = os.path.join(os.path.abspath(srcdir), dest) if os.path.exists(destf): dest = os.path.join(destf, 'viz.js') try: shutil.copy(path, dest) logger.info( "[gdot] copy '{}' to '{}'.".format(path, dest)) except PermissionError as e: # pragma: no cover logger.warning("[gdot] permission error: {}, " "unable to use local viz.js.".format(e)) if not os.path.exists(dest): logger.warning("[gdot] unable to copy='{}', " "unable to use local viz.js.".format(dest)) else: logger.warning("[gdot] destination folder='{}' does not exists, " "unable to use local viz.js.".format(destf)) else: logger.warning("[gdot] unable to locate html_static_path='{}', " "unable to use local viz.js.".format(app.config.html_static_path)) else: logger.warning("[gdot] jyquickhelper needs to be update, unable to find '{}'.".format( path)) else: logger.warning("[gdot] jyquickhelper not installed, falling back to " "'{}'".format(GDotDirective._default_url))
[docs]def setup(app): """ setup for ``gdot`` (sphinx) :githublink:`%|py|296` """ if 'sphinx.ext.graphviz' not in app.config.extensions: from sphinx.ext.graphviz import setup as setup_g # pylint: disable=W0611 setup_g(app) app.connect('builder-inited', copy_js_files) from sphinx.ext.graphviz import latex_visit_graphviz, man_visit_graphviz # pylint: disable=W0611 from sphinx.ext.graphviz import text_visit_graphviz # pylint: disable=W0611 app.add_node(gdot_node, html=(visit_gdot_node_html, depart_gdot_node_html), epub=(visit_gdot_node_html, depart_gdot_node_html), elatex=(latex_visit_graphviz, None), latex=(latex_visit_graphviz, None), text=(text_visit_graphviz, None), md=(text_visit_graphviz, None), rst=(visit_gdot_node_rst, depart_gdot_node_rst)) app.add_directive('gdot', GDotDirective) return {'version': sphinx.__display_version__, 'parallel_read_safe': True}