Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Defines a sphinx extension to show :epkg:`DOT` graph
5with :epkg:`viz.js` or :epkg:`graphviz`.
7.. versionadded:: 1.9
8"""
9import os
10import logging
11import shutil
12from docutils import nodes
13from docutils.parsers.rst import directives
14import sphinx
15from docutils.parsers.rst import Directive
16from .sphinxext_helper import get_env_state_info
17from .sphinx_runpython_extension import run_python_script
20class gdot_node(nodes.admonition):
21 """
22 defines ``gdot`` node.
23 """
24 pass
27class GDotDirective(Directive):
28 """
29 A ``gdot`` node displays a :epkg:`DOT` graph.
30 The build choose :epkg:`SVG` for :epkg:`HTML` format and image for
31 other format unless it is specified.
33 * *format*: SVG or HTML
34 * *script*: boolean or a string to indicate than the standard output
35 should only be considered after this substring
36 * *url*: url to :epkg:`viz.js`, only if format *SVG* is selected
37 * *process*: run the script in an another process
39 Example::
41 .. gdot::
43 digraph foo {
44 "bar" -> "baz";
45 }
47 Which gives:
49 .. gdot::
51 digraph foo {
52 "bar" -> "baz";
53 }
55 The directive also accepts scripts producing
56 dot graphs on the standard output. Option *script*
57 must be specified. This extension loads
58 `sphinx.ext.graphviz <https://www.sphinx-doc.org/
59 en/master/usage/extensions/graphviz.html>`_
60 if not added to the list of extensions:
62 Example::
64 .. gdot::
65 :format: png
67 digraph foo {
68 "bar" -> "baz";
69 }
71 .. gdot::
72 :format: png
74 digraph foo {
75 "bar" -> "baz";
76 }
78 The output can be produced by a script.
80 .. gdot::
81 :script:
83 print('''
84 digraph foo {
85 "bar" -> "baz";
86 }
87 ''')
89 .. gdot::
90 :script:
92 print('''
93 digraph foo {
94 "bar" -> "baz";
95 }
96 ''')
97 """
98 node_class = gdot_node
99 has_content = True
100 required_arguments = 0
101 optional_arguments = 0
102 final_argument_whitespace = False
103 option_spec = {
104 'format': directives.unchanged,
105 'script': directives.unchanged,
106 'url': directives.unchanged,
107 'process': directives.unchanged,
108 }
110 _default_url = "http://www.xavierdupre.fr/js/vizjs/viz.js"
112 def run(self):
113 """
114 Builds the collapse text.
115 """
116 # retrieves the parameters
117 if 'format' in self.options:
118 format = self.options['format']
119 else:
120 format = '?'
121 url = self.options.get('url', 'local')
122 bool_set_ = (True, 1, "True", "1", "true", '')
123 process = 'process' in self.options and self.options['process'] in bool_set_
124 if url == 'local':
125 try:
126 import jyquickhelper
127 path = os.path.join(os.path.dirname(
128 jyquickhelper.__file__), "js", "vizjs", "viz.js")
129 if not os.path.exists(path):
130 raise ImportError(
131 "jyquickelper needs to be updated to get viz.js.")
132 url = 'local'
133 except ImportError:
134 url = GDotDirective._default_url
135 logger = logging.getLogger("gdot")
136 logger.warning("[gdot] jyquickhelper not installed, falling back to "
137 "'{}'".format(url))
139 info = get_env_state_info(self)
140 docname = info['docname']
141 if url == 'local':
142 if docname is None or 'HERE' not in info:
143 url = GDotDirective._default_url
144 logger = logging.getLogger("gdot")
145 logger.warning("[gdot] docname is none, falling back to "
146 "'{}'".format(url))
147 else:
148 spl = docname.split("/")
149 sp = ['..'] * (len(spl) - 1) + ['_static', 'viz.js']
150 url = "/".join(sp)
152 if 'script' in self.options:
153 script = self.options['script']
154 if script in (0, "0", "False", 'false'):
155 script = None
156 elif script in (1, "1", "True", 'true', ''):
157 script = ''
158 elif len(script) == 0:
159 raise RuntimeError("script should be a string to indicate"
160 " the beginning of DOT graph.")
161 else:
162 script = False
164 # executes script if any
165 content = "\n".join(self.content)
166 if script or script == '':
167 stdout, stderr, _ = run_python_script(content, process=process)
168 if stderr:
169 raise RuntimeError(
170 "A graph cannot be draw due to {}".format(stderr))
171 content = stdout
172 if script:
173 spl = content.split(script)
174 if len(spl) > 2:
175 raise RuntimeError("'{}' indicates the beginning of the graph "
176 "but there are many in\n{}".format(script, content))
177 content = spl[-1]
179 node = gdot_node(format=format, code=content, url=url,
180 options={'docname': docname})
181 return [node]
184def visit_gdot_node_rst(self, node):
185 """
186 visit collapse_node
187 """
188 self.new_state(0)
189 self.add_text('.. gdot::' + self.nl)
190 if node['format'] != '?':
191 self.add_text(' :format: ' + node['format'] + self.nl)
192 if node['url']:
193 self.add_text(' :url: ' + node['url'] + self.nl)
194 self.new_state(self.indent)
195 for row in node['code'].split('\n'):
196 self.add_text(row + self.nl)
199def depart_gdot_node_rst(self, node):
200 """
201 depart collapse_node
202 """
203 self.end_state()
204 self.end_state(wrap=False)
207def visit_gdot_node_html_svg(self, node):
208 """
209 visit collapse_node
210 """
211 def process(text):
212 text = text.replace("\\", "\\\\")
213 text = text.replace("\n", "\\n")
214 text = text.replace('"', '\\"')
215 return text
217 nid = str(id(node))
219 content = """
220 <div id="gdot-{0}-cont"><div id="gdot-{0}" style="width:100%;height:100%;"></div>
221 """.format(nid)
223 script = """
224 require(['__URL__'], function() { var svgGraph = Viz("__DOT__");
225 document.getElementById('gdot-__ID__').innerHTML = svgGraph; });
226 """.replace('__ID__', nid).replace('__DOT__', process(node['code'])).replace(
227 "__URL__", node['url'])
229 self.body.append(content)
230 self.body.append("<script>{0}{1}{0}</script>{0}".format("\n", script))
233def depart_gdot_node_html_svg(self, node):
234 """
235 depart collapse_node
236 """
237 self.body.append("</div>")
240def visit_gdot_node_html(self, node):
241 """
242 visit collapse_node, the function switches between
243 `graphviz.py <https://github.com/sphinx-doc/sphinx/blob/
244 master/sphinx/ext/graphviz.py>`_ and the :epkg:`SVG` format.
245 """
246 if node['format'].lower() == 'png':
247 from sphinx.ext.graphviz import html_visit_graphviz
248 return html_visit_graphviz(self, node)
249 if node['format'].lower() in ('?', 'svg'):
250 return visit_gdot_node_html_svg(self, node)
251 raise RuntimeError(
252 "Unexpected format for graphviz '{}'.".format(node['format']))
255def depart_gdot_node_html(self, node):
256 """
257 depart collapse_node
258 """
259 if node['format'] == 'png':
260 return None
261 return depart_gdot_node_html_svg(self, node)
264def copy_js_files(app):
265 try:
266 import jyquickhelper
267 local = True
268 except ImportError:
269 local = False
271 logger = logging.getLogger("gdot")
272 if local:
273 path = os.path.join(os.path.dirname(
274 jyquickhelper.__file__), "js", "vizjs", "viz.js")
275 if os.path.exists(path):
276 # We copy the file to static path.
277 dest = app.config.html_static_path
278 if isinstance(dest, list) and len(dest) > 0:
279 dest = dest[0]
280 else:
281 dest = None
283 srcdir = app.builder.srcdir
284 if "IMPOSSIBLE:TOFIND" not in srcdir:
285 if not os.path.exists(srcdir):
286 raise FileNotFoundError(
287 "Source file is wrong '{}'.".format(srcdir))
289 if dest is not None:
290 destf = os.path.join(os.path.abspath(srcdir), dest)
291 if os.path.exists(destf):
292 dest = os.path.join(destf, 'viz.js')
293 try:
294 shutil.copy(path, dest)
295 logger.info(
296 "[gdot] copy '{}' to '{}'.".format(path, dest))
297 except PermissionError as e: # pragma: no cover
298 logger.warning("[gdot] permission error: {}, "
299 "unable to use local viz.js.".format(e))
301 if not os.path.exists(dest):
302 logger.warning("[gdot] unable to copy='{}', "
303 "unable to use local viz.js.".format(dest))
304 else:
305 logger.warning("[gdot] destination folder='{}' does not exists, "
306 "unable to use local viz.js.".format(destf))
307 else:
308 logger.warning("[gdot] unable to locate html_static_path='{}', "
309 "unable to use local viz.js.".format(app.config.html_static_path))
310 else:
311 logger.warning("[gdot] jyquickhelper needs to be update, unable to find '{}'.".format(
312 path))
313 else:
314 logger.warning("[gdot] jyquickhelper not installed, falling back to "
315 "'{}'".format(GDotDirective._default_url))
318def setup(app):
319 """
320 setup for ``gdot`` (sphinx)
321 """
322 if 'sphinx.ext.graphviz' not in app.config.extensions:
323 from sphinx.ext.graphviz import setup as setup_g # pylint: disable=W0611
324 setup_g(app)
326 app.connect('builder-inited', copy_js_files)
328 from sphinx.ext.graphviz import latex_visit_graphviz, man_visit_graphviz # pylint: disable=W0611
329 from sphinx.ext.graphviz import text_visit_graphviz # pylint: disable=W0611
330 app.add_node(gdot_node,
331 html=(visit_gdot_node_html, depart_gdot_node_html),
332 epub=(visit_gdot_node_html, depart_gdot_node_html),
333 elatex=(latex_visit_graphviz, None),
334 latex=(latex_visit_graphviz, None),
335 text=(text_visit_graphviz, None),
336 md=(text_visit_graphviz, None),
337 rst=(visit_gdot_node_rst, depart_gdot_node_rst))
339 app.add_directive('gdot', GDotDirective)
340 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}