Hide keyboard shortcuts

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`. 

6 

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 

18 

19 

20class gdot_node(nodes.admonition): 

21 """ 

22 defines ``gdot`` node. 

23 """ 

24 pass 

25 

26 

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. 

32 

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 

38 

39 Example:: 

40 

41 .. gdot:: 

42 

43 digraph foo { 

44 "bar" -> "baz"; 

45 } 

46 

47 Which gives: 

48 

49 .. gdot:: 

50 

51 digraph foo { 

52 "bar" -> "baz"; 

53 } 

54 

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: 

61 

62 Example:: 

63 

64 .. gdot:: 

65 :format: png 

66 

67 digraph foo { 

68 "bar" -> "baz"; 

69 } 

70 

71 .. gdot:: 

72 :format: png 

73 

74 digraph foo { 

75 "bar" -> "baz"; 

76 } 

77 

78 The output can be produced by a script. 

79 

80 .. gdot:: 

81 :script: 

82 

83 print(''' 

84 digraph foo { 

85 "bar" -> "baz"; 

86 } 

87 ''') 

88 

89 .. gdot:: 

90 :script: 

91 

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 } 

109 

110 _default_url = "http://www.xavierdupre.fr/js/vizjs/viz.js" 

111 

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)) 

138 

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) 

151 

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 

163 

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] 

178 

179 node = gdot_node(format=format, code=content, url=url, 

180 options={'docname': docname}) 

181 return [node] 

182 

183 

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) 

197 

198 

199def depart_gdot_node_rst(self, node): 

200 """ 

201 depart collapse_node 

202 """ 

203 self.end_state() 

204 self.end_state(wrap=False) 

205 

206 

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 

216 

217 nid = str(id(node)) 

218 

219 content = """ 

220 <div id="gdot-{0}-cont"><div id="gdot-{0}" style="width:100%;height:100%;"></div> 

221 """.format(nid) 

222 

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']) 

228 

229 self.body.append(content) 

230 self.body.append("<script>{0}{1}{0}</script>{0}".format("\n", script)) 

231 

232 

233def depart_gdot_node_html_svg(self, node): 

234 """ 

235 depart collapse_node 

236 """ 

237 self.body.append("</div>") 

238 

239 

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'])) 

253 

254 

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) 

262 

263 

264def copy_js_files(app): 

265 try: 

266 import jyquickhelper 

267 local = True 

268 except ImportError: 

269 local = False 

270 

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 

282 

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)) 

288 

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)) 

300 

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)) 

316 

317 

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) 

325 

326 app.connect('builder-inited', copy_js_files) 

327 

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)) 

338 

339 app.add_directive('gdot', GDotDirective) 

340 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}