Commit | Line | Data |
---|---|---|
db6ccf23 MH |
1 | # -*- coding: utf-8; mode: python -*- |
2 | # pylint: disable=C0103, R0903, R0912, R0915 | |
3 | u""" | |
4 | scalable figure and image handling | |
5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
6 | ||
7 | Sphinx extension which implements scalable image handling. | |
8 | ||
9 | :copyright: Copyright (C) 2016 Markus Heiser | |
10 | :license: GPL Version 2, June 1991 see Linux/COPYING for details. | |
11 | ||
12 | The build for image formats depend on image's source format and output's | |
13 | destination format. This extension implement methods to simplify image | |
14 | handling from the author's POV. Directives like ``kernel-figure`` implement | |
15 | methods *to* always get the best output-format even if some tools are not | |
16 | installed. For more details take a look at ``convert_image(...)`` which is | |
17 | the core of all conversions. | |
18 | ||
19 | * ``.. kernel-image``: for image handling / a ``.. image::`` replacement | |
20 | ||
21 | * ``.. kernel-figure``: for figure handling / a ``.. figure::`` replacement | |
22 | ||
23 | * ``.. kernel-render``: for render markup / a concept to embed *render* | |
24 | markups (or languages). Supported markups (see ``RENDER_MARKUP_EXT``) | |
25 | ||
26 | - ``DOT``: render embedded Graphviz's **DOC** | |
27 | - ``SVG``: render embedded Scalable Vector Graphics (**SVG**) | |
28 | - ... *developable* | |
29 | ||
30 | Used tools: | |
31 | ||
93431e06 | 32 | * ``dot(1)``: Graphviz (https://www.graphviz.org). If Graphviz is not |
db6ccf23 | 33 | available, the DOT language is inserted as literal-block. |
c9edbe1e AY |
34 | For conversion to PDF, ``rsvg-convert(1)`` of librsvg |
35 | (https://gitlab.gnome.org/GNOME/librsvg) is used when available. | |
db6ccf23 MH |
36 | |
37 | * SVG to PDF: To generate PDF, you need at least one of this tools: | |
38 | ||
39 | - ``convert(1)``: ImageMagick (https://www.imagemagick.org) | |
8ccd0569 | 40 | - ``inkscape(1)``: Inkscape (https://inkscape.org/) |
db6ccf23 MH |
41 | |
42 | List of customizations: | |
43 | ||
44 | * generate PDF from SVG / used by PDF (LaTeX) builder | |
45 | ||
46 | * generate SVG (html-builder) and PDF (latex-builder) from DOT files. | |
93431e06 | 47 | DOT: see https://www.graphviz.org/content/dot-language |
db6ccf23 MH |
48 | |
49 | """ | |
50 | ||
51 | import os | |
52 | from os import path | |
53 | import subprocess | |
54 | from hashlib import sha1 | |
ecf5fb58 | 55 | import re |
db6ccf23 MH |
56 | from docutils import nodes |
57 | from docutils.statemachine import ViewList | |
58 | from docutils.parsers.rst import directives | |
59 | from docutils.parsers.rst.directives import images | |
60 | import sphinx | |
db6ccf23 | 61 | from sphinx.util.nodes import clean_astext |
096ea522 JC |
62 | import kernellog |
63 | ||
db6ccf23 MH |
64 | # Get Sphinx version |
65 | major, minor, patch = sphinx.version_info[:3] | |
66 | if major == 1 and minor > 3: | |
67 | # patches.Figure only landed in Sphinx 1.4 | |
68 | from sphinx.directives.patches import Figure # pylint: disable=C0413 | |
69 | else: | |
70 | Figure = images.Figure | |
71 | ||
72 | __version__ = '1.0.0' | |
73 | ||
74 | # simple helper | |
75 | # ------------- | |
76 | ||
77 | def which(cmd): | |
ae17a87d | 78 | """Searches the ``cmd`` in the ``PATH`` environment. |
db6ccf23 MH |
79 | |
80 | This *which* searches the PATH for executable ``cmd`` . First match is | |
81 | returned, if nothing is found, ``None` is returned. | |
82 | """ | |
83 | envpath = os.environ.get('PATH', None) or os.defpath | |
84 | for folder in envpath.split(os.pathsep): | |
85 | fname = folder + os.sep + cmd | |
86 | if path.isfile(fname): | |
87 | return fname | |
88 | ||
89 | def mkdir(folder, mode=0o775): | |
90 | if not path.isdir(folder): | |
91 | os.makedirs(folder, mode) | |
92 | ||
93 | def file2literal(fname): | |
94 | with open(fname, "r") as src: | |
95 | data = src.read() | |
96 | node = nodes.literal_block(data, data) | |
97 | return node | |
98 | ||
99 | def isNewer(path1, path2): | |
100 | """Returns True if ``path1`` is newer than ``path2`` | |
101 | ||
102 | If ``path1`` exists and is newer than ``path2`` the function returns | |
103 | ``True`` is returned otherwise ``False`` | |
104 | """ | |
105 | return (path.exists(path1) | |
106 | and os.stat(path1).st_ctime > os.stat(path2).st_ctime) | |
107 | ||
108 | def pass_handle(self, node): # pylint: disable=W0613 | |
109 | pass | |
110 | ||
111 | # setup conversion tools and sphinx extension | |
112 | # ------------------------------------------- | |
113 | ||
114 | # Graphviz's dot(1) support | |
115 | dot_cmd = None | |
ecf5fb58 AY |
116 | # dot(1) -Tpdf should be used |
117 | dot_Tpdf = False | |
db6ccf23 MH |
118 | |
119 | # ImageMagick' convert(1) support | |
120 | convert_cmd = None | |
121 | ||
c9edbe1e AY |
122 | # librsvg's rsvg-convert(1) support |
123 | rsvg_convert_cmd = None | |
124 | ||
8ccd0569 AY |
125 | # Inkscape's inkscape(1) support |
126 | inkscape_cmd = None | |
127 | # Inkscape prior to 1.0 uses different command options | |
128 | inkscape_ver_one = False | |
129 | ||
db6ccf23 MH |
130 | |
131 | def setup(app): | |
132 | # check toolchain first | |
133 | app.connect('builder-inited', setupTools) | |
134 | ||
135 | # image handling | |
136 | app.add_directive("kernel-image", KernelImage) | |
137 | app.add_node(kernel_image, | |
138 | html = (visit_kernel_image, pass_handle), | |
139 | latex = (visit_kernel_image, pass_handle), | |
140 | texinfo = (visit_kernel_image, pass_handle), | |
141 | text = (visit_kernel_image, pass_handle), | |
142 | man = (visit_kernel_image, pass_handle), ) | |
143 | ||
144 | # figure handling | |
145 | app.add_directive("kernel-figure", KernelFigure) | |
146 | app.add_node(kernel_figure, | |
147 | html = (visit_kernel_figure, pass_handle), | |
148 | latex = (visit_kernel_figure, pass_handle), | |
149 | texinfo = (visit_kernel_figure, pass_handle), | |
150 | text = (visit_kernel_figure, pass_handle), | |
151 | man = (visit_kernel_figure, pass_handle), ) | |
152 | ||
153 | # render handling | |
154 | app.add_directive('kernel-render', KernelRender) | |
155 | app.add_node(kernel_render, | |
156 | html = (visit_kernel_render, pass_handle), | |
157 | latex = (visit_kernel_render, pass_handle), | |
158 | texinfo = (visit_kernel_render, pass_handle), | |
159 | text = (visit_kernel_render, pass_handle), | |
160 | man = (visit_kernel_render, pass_handle), ) | |
161 | ||
162 | app.connect('doctree-read', add_kernel_figure_to_std_domain) | |
163 | ||
164 | return dict( | |
165 | version = __version__, | |
166 | parallel_read_safe = True, | |
167 | parallel_write_safe = True | |
168 | ) | |
169 | ||
170 | ||
171 | def setupTools(app): | |
172 | u""" | |
173 | Check available build tools and log some *verbose* messages. | |
174 | ||
175 | This function is called once, when the builder is initiated. | |
176 | """ | |
ecf5fb58 | 177 | global dot_cmd, dot_Tpdf, convert_cmd, rsvg_convert_cmd # pylint: disable=W0603 |
8ccd0569 | 178 | global inkscape_cmd, inkscape_ver_one # pylint: disable=W0603 |
096ea522 | 179 | kernellog.verbose(app, "kfigure: check installed tools ...") |
db6ccf23 MH |
180 | |
181 | dot_cmd = which('dot') | |
182 | convert_cmd = which('convert') | |
c9edbe1e | 183 | rsvg_convert_cmd = which('rsvg-convert') |
8ccd0569 | 184 | inkscape_cmd = which('inkscape') |
db6ccf23 MH |
185 | |
186 | if dot_cmd: | |
096ea522 | 187 | kernellog.verbose(app, "use dot(1) from: " + dot_cmd) |
ecf5fb58 AY |
188 | |
189 | try: | |
190 | dot_Thelp_list = subprocess.check_output([dot_cmd, '-Thelp'], | |
191 | stderr=subprocess.STDOUT) | |
192 | except subprocess.CalledProcessError as err: | |
193 | dot_Thelp_list = err.output | |
194 | pass | |
195 | ||
196 | dot_Tpdf_ptn = b'pdf' | |
197 | dot_Tpdf = re.search(dot_Tpdf_ptn, dot_Thelp_list) | |
db6ccf23 | 198 | else: |
096ea522 | 199 | kernellog.warn(app, "dot(1) not found, for better output quality install " |
93431e06 | 200 | "graphviz from https://www.graphviz.org") |
8ccd0569 AY |
201 | if inkscape_cmd: |
202 | kernellog.verbose(app, "use inkscape(1) from: " + inkscape_cmd) | |
f30a7ac8 AY |
203 | inkscape_ver = subprocess.check_output([inkscape_cmd, '--version'], |
204 | stderr=subprocess.DEVNULL) | |
8ccd0569 AY |
205 | ver_one_ptn = b'Inkscape 1' |
206 | inkscape_ver_one = re.search(ver_one_ptn, inkscape_ver) | |
207 | convert_cmd = None | |
208 | rsvg_convert_cmd = None | |
ecf5fb58 | 209 | dot_Tpdf = False |
8ccd0569 | 210 | |
c9edbe1e | 211 | else: |
8ccd0569 AY |
212 | if convert_cmd: |
213 | kernellog.verbose(app, "use convert(1) from: " + convert_cmd) | |
214 | else: | |
d987d5ae | 215 | kernellog.verbose(app, |
8ccd0569 AY |
216 | "Neither inkscape(1) nor convert(1) found.\n" |
217 | "For SVG to PDF conversion, " | |
218 | "install either Inkscape (https://inkscape.org/) (preferred) or\n" | |
219 | "ImageMagick (https://www.imagemagick.org)") | |
220 | ||
221 | if rsvg_convert_cmd: | |
222 | kernellog.verbose(app, "use rsvg-convert(1) from: " + rsvg_convert_cmd) | |
223 | kernellog.verbose(app, "use 'dot -Tsvg' and rsvg-convert(1) for DOT -> PDF conversion") | |
224 | dot_Tpdf = False | |
ecf5fb58 | 225 | else: |
8ccd0569 AY |
226 | kernellog.verbose(app, |
227 | "rsvg-convert(1) not found.\n" | |
228 | " SVG rendering of convert(1) is done by ImageMagick-native renderer.") | |
229 | if dot_Tpdf: | |
230 | kernellog.verbose(app, "use 'dot -Tpdf' for DOT -> PDF conversion") | |
231 | else: | |
232 | kernellog.verbose(app, "use 'dot -Tsvg' and convert(1) for DOT -> PDF conversion") | |
db6ccf23 MH |
233 | |
234 | ||
235 | # integrate conversion tools | |
236 | # -------------------------- | |
237 | ||
238 | RENDER_MARKUP_EXT = { | |
239 | # The '.ext' must be handled by convert_image(..) function's *in_ext* input. | |
240 | # <name> : <.ext> | |
241 | 'DOT' : '.dot', | |
242 | 'SVG' : '.svg' | |
243 | } | |
244 | ||
245 | def convert_image(img_node, translator, src_fname=None): | |
246 | """Convert a image node for the builder. | |
247 | ||
248 | Different builder prefer different image formats, e.g. *latex* builder | |
249 | prefer PDF while *html* builder prefer SVG format for images. | |
250 | ||
251 | This function handles output image formats in dependence of source the | |
252 | format (of the image) and the translator's output format. | |
253 | """ | |
254 | app = translator.builder.app | |
255 | ||
256 | fname, in_ext = path.splitext(path.basename(img_node['uri'])) | |
257 | if src_fname is None: | |
258 | src_fname = path.join(translator.builder.srcdir, img_node['uri']) | |
259 | if not path.exists(src_fname): | |
260 | src_fname = path.join(translator.builder.outdir, img_node['uri']) | |
261 | ||
262 | dst_fname = None | |
263 | ||
264 | # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages | |
265 | ||
096ea522 | 266 | kernellog.verbose(app, 'assert best format for: ' + img_node['uri']) |
db6ccf23 MH |
267 | |
268 | if in_ext == '.dot': | |
269 | ||
270 | if not dot_cmd: | |
096ea522 JC |
271 | kernellog.verbose(app, |
272 | "dot from graphviz not available / include DOT raw.") | |
db6ccf23 MH |
273 | img_node.replace_self(file2literal(src_fname)) |
274 | ||
275 | elif translator.builder.format == 'latex': | |
276 | dst_fname = path.join(translator.builder.outdir, fname + '.pdf') | |
277 | img_node['uri'] = fname + '.pdf' | |
278 | img_node['candidates'] = {'*': fname + '.pdf'} | |
279 | ||
280 | ||
281 | elif translator.builder.format == 'html': | |
282 | dst_fname = path.join( | |
283 | translator.builder.outdir, | |
284 | translator.builder.imagedir, | |
285 | fname + '.svg') | |
286 | img_node['uri'] = path.join( | |
287 | translator.builder.imgpath, fname + '.svg') | |
288 | img_node['candidates'] = { | |
289 | '*': path.join(translator.builder.imgpath, fname + '.svg')} | |
290 | ||
291 | else: | |
292 | # all other builder formats will include DOT as raw | |
293 | img_node.replace_self(file2literal(src_fname)) | |
294 | ||
295 | elif in_ext == '.svg': | |
296 | ||
297 | if translator.builder.format == 'latex': | |
8ccd0569 | 298 | if not inkscape_cmd and convert_cmd is None: |
d987d5ae AY |
299 | kernellog.warn(app, |
300 | "no SVG to PDF conversion available / include SVG raw." | |
301 | "\nIncluding large raw SVGs can cause xelatex error." | |
302 | "\nInstall Inkscape (preferred) or ImageMagick.") | |
db6ccf23 MH |
303 | img_node.replace_self(file2literal(src_fname)) |
304 | else: | |
305 | dst_fname = path.join(translator.builder.outdir, fname + '.pdf') | |
306 | img_node['uri'] = fname + '.pdf' | |
307 | img_node['candidates'] = {'*': fname + '.pdf'} | |
308 | ||
309 | if dst_fname: | |
310 | # the builder needs not to copy one more time, so pop it if exists. | |
311 | translator.builder.images.pop(img_node['uri'], None) | |
312 | _name = dst_fname[len(translator.builder.outdir) + 1:] | |
313 | ||
314 | if isNewer(dst_fname, src_fname): | |
096ea522 JC |
315 | kernellog.verbose(app, |
316 | "convert: {out}/%s already exists and is newer" % _name) | |
db6ccf23 MH |
317 | |
318 | else: | |
319 | ok = False | |
320 | mkdir(path.dirname(dst_fname)) | |
321 | ||
322 | if in_ext == '.dot': | |
096ea522 | 323 | kernellog.verbose(app, 'convert DOT to: {out}/' + _name) |
ecf5fb58 | 324 | if translator.builder.format == 'latex' and not dot_Tpdf: |
c9edbe1e AY |
325 | svg_fname = path.join(translator.builder.outdir, fname + '.svg') |
326 | ok1 = dot2format(app, src_fname, svg_fname) | |
327 | ok2 = svg2pdf_by_rsvg(app, svg_fname, dst_fname) | |
328 | ok = ok1 and ok2 | |
ecf5fb58 | 329 | |
c9edbe1e AY |
330 | else: |
331 | ok = dot2format(app, src_fname, dst_fname) | |
db6ccf23 MH |
332 | |
333 | elif in_ext == '.svg': | |
096ea522 | 334 | kernellog.verbose(app, 'convert SVG to: {out}/' + _name) |
db6ccf23 MH |
335 | ok = svg2pdf(app, src_fname, dst_fname) |
336 | ||
337 | if not ok: | |
338 | img_node.replace_self(file2literal(src_fname)) | |
339 | ||
340 | ||
341 | def dot2format(app, dot_fname, out_fname): | |
342 | """Converts DOT file to ``out_fname`` using ``dot(1)``. | |
343 | ||
344 | * ``dot_fname`` pathname of the input DOT file, including extension ``.dot`` | |
345 | * ``out_fname`` pathname of the output file, including format extension | |
346 | ||
347 | The *format extension* depends on the ``dot`` command (see ``man dot`` | |
348 | option ``-Txxx``). Normally you will use one of the following extensions: | |
349 | ||
350 | - ``.ps`` for PostScript, | |
351 | - ``.svg`` or ``svgz`` for Structured Vector Graphics, | |
352 | - ``.fig`` for XFIG graphics and | |
353 | - ``.png`` or ``gif`` for common bitmap graphics. | |
354 | ||
355 | """ | |
356 | out_format = path.splitext(out_fname)[1][1:] | |
357 | cmd = [dot_cmd, '-T%s' % out_format, dot_fname] | |
358 | exit_code = 42 | |
359 | ||
360 | with open(out_fname, "w") as out: | |
361 | exit_code = subprocess.call(cmd, stdout = out) | |
362 | if exit_code != 0: | |
096ea522 JC |
363 | kernellog.warn(app, |
364 | "Error #%d when calling: %s" % (exit_code, " ".join(cmd))) | |
db6ccf23 MH |
365 | return bool(exit_code == 0) |
366 | ||
367 | def svg2pdf(app, svg_fname, pdf_fname): | |
8ccd0569 | 368 | """Converts SVG to PDF with ``inkscape(1)`` or ``convert(1)`` command. |
db6ccf23 | 369 | |
8ccd0569 AY |
370 | Uses ``inkscape(1)`` from Inkscape (https://inkscape.org/) or ``convert(1)`` |
371 | from ImageMagick (https://www.imagemagick.org) for conversion. | |
372 | Returns ``True`` on success and ``False`` if an error occurred. | |
db6ccf23 MH |
373 | |
374 | * ``svg_fname`` pathname of the input SVG file with extension (``.svg``) | |
375 | * ``pdf_name`` pathname of the output PDF file with extension (``.pdf``) | |
376 | ||
377 | """ | |
378 | cmd = [convert_cmd, svg_fname, pdf_fname] | |
f30a7ac8 | 379 | cmd_name = 'convert(1)' |
8ccd0569 AY |
380 | |
381 | if inkscape_cmd: | |
f30a7ac8 | 382 | cmd_name = 'inkscape(1)' |
8ccd0569 AY |
383 | if inkscape_ver_one: |
384 | cmd = [inkscape_cmd, '-o', pdf_fname, svg_fname] | |
385 | else: | |
386 | cmd = [inkscape_cmd, '-z', '--export-pdf=%s' % pdf_fname, svg_fname] | |
387 | ||
f30a7ac8 AY |
388 | try: |
389 | warning_msg = subprocess.check_output(cmd, stderr=subprocess.STDOUT) | |
390 | exit_code = 0 | |
391 | except subprocess.CalledProcessError as err: | |
392 | warning_msg = err.output | |
393 | exit_code = err.returncode | |
394 | pass | |
395 | ||
db6ccf23 | 396 | if exit_code != 0: |
096ea522 | 397 | kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd))) |
f30a7ac8 AY |
398 | if warning_msg: |
399 | kernellog.warn(app, "Warning msg from %s: %s" | |
400 | % (cmd_name, str(warning_msg, 'utf-8'))) | |
401 | elif warning_msg: | |
402 | kernellog.verbose(app, "Warning msg from %s (likely harmless):\n%s" | |
403 | % (cmd_name, str(warning_msg, 'utf-8'))) | |
404 | ||
db6ccf23 MH |
405 | return bool(exit_code == 0) |
406 | ||
c9edbe1e AY |
407 | def svg2pdf_by_rsvg(app, svg_fname, pdf_fname): |
408 | """Convert SVG to PDF with ``rsvg-convert(1)`` command. | |
409 | ||
410 | * ``svg_fname`` pathname of input SVG file, including extension ``.svg`` | |
411 | * ``pdf_fname`` pathname of output PDF file, including extension ``.pdf`` | |
412 | ||
413 | Input SVG file should be the one generated by ``dot2format()``. | |
414 | SVG -> PDF conversion is done by ``rsvg-convert(1)``. | |
415 | ||
416 | If ``rsvg-convert(1)`` is unavailable, fall back to ``svg2pdf()``. | |
417 | ||
418 | """ | |
419 | ||
420 | if rsvg_convert_cmd is None: | |
421 | ok = svg2pdf(app, svg_fname, pdf_fname) | |
422 | else: | |
423 | cmd = [rsvg_convert_cmd, '--format=pdf', '-o', pdf_fname, svg_fname] | |
424 | # use stdout and stderr from parent | |
425 | exit_code = subprocess.call(cmd) | |
426 | if exit_code != 0: | |
427 | kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd))) | |
428 | ok = bool(exit_code == 0) | |
429 | ||
430 | return ok | |
431 | ||
db6ccf23 MH |
432 | |
433 | # image handling | |
434 | # --------------------- | |
435 | ||
436 | def visit_kernel_image(self, node): # pylint: disable=W0613 | |
437 | """Visitor of the ``kernel_image`` Node. | |
438 | ||
439 | Handles the ``image`` child-node with the ``convert_image(...)``. | |
440 | """ | |
441 | img_node = node[0] | |
442 | convert_image(img_node, self) | |
443 | ||
444 | class kernel_image(nodes.image): | |
445 | """Node for ``kernel-image`` directive.""" | |
446 | pass | |
447 | ||
448 | class KernelImage(images.Image): | |
449 | u"""KernelImage directive | |
450 | ||
451 | Earns everything from ``.. image::`` directive, except *remote URI* and | |
452 | *glob* pattern. The KernelImage wraps a image node into a | |
453 | kernel_image node. See ``visit_kernel_image``. | |
454 | """ | |
455 | ||
456 | def run(self): | |
457 | uri = self.arguments[0] | |
458 | if uri.endswith('.*') or uri.find('://') != -1: | |
459 | raise self.severe( | |
460 | 'Error in "%s: %s": glob pattern and remote images are not allowed' | |
461 | % (self.name, uri)) | |
462 | result = images.Image.run(self) | |
463 | if len(result) == 2 or isinstance(result[0], nodes.system_message): | |
464 | return result | |
465 | (image_node,) = result | |
466 | # wrap image node into a kernel_image node / see visitors | |
467 | node = kernel_image('', image_node) | |
468 | return [node] | |
469 | ||
470 | # figure handling | |
471 | # --------------------- | |
472 | ||
473 | def visit_kernel_figure(self, node): # pylint: disable=W0613 | |
474 | """Visitor of the ``kernel_figure`` Node. | |
475 | ||
476 | Handles the ``image`` child-node with the ``convert_image(...)``. | |
477 | """ | |
478 | img_node = node[0][0] | |
479 | convert_image(img_node, self) | |
480 | ||
481 | class kernel_figure(nodes.figure): | |
482 | """Node for ``kernel-figure`` directive.""" | |
483 | ||
484 | class KernelFigure(Figure): | |
485 | u"""KernelImage directive | |
486 | ||
487 | Earns everything from ``.. figure::`` directive, except *remote URI* and | |
488 | *glob* pattern. The KernelFigure wraps a figure node into a kernel_figure | |
489 | node. See ``visit_kernel_figure``. | |
490 | """ | |
491 | ||
492 | def run(self): | |
493 | uri = self.arguments[0] | |
494 | if uri.endswith('.*') or uri.find('://') != -1: | |
495 | raise self.severe( | |
496 | 'Error in "%s: %s":' | |
497 | ' glob pattern and remote images are not allowed' | |
498 | % (self.name, uri)) | |
499 | result = Figure.run(self) | |
500 | if len(result) == 2 or isinstance(result[0], nodes.system_message): | |
501 | return result | |
502 | (figure_node,) = result | |
503 | # wrap figure node into a kernel_figure node / see visitors | |
504 | node = kernel_figure('', figure_node) | |
505 | return [node] | |
506 | ||
507 | ||
508 | # render handling | |
509 | # --------------------- | |
510 | ||
511 | def visit_kernel_render(self, node): | |
512 | """Visitor of the ``kernel_render`` Node. | |
513 | ||
514 | If rendering tools available, save the markup of the ``literal_block`` child | |
515 | node into a file and replace the ``literal_block`` node with a new created | |
516 | ``image`` node, pointing to the saved markup file. Afterwards, handle the | |
517 | image child-node with the ``convert_image(...)``. | |
518 | """ | |
519 | app = self.builder.app | |
520 | srclang = node.get('srclang') | |
521 | ||
096ea522 | 522 | kernellog.verbose(app, 'visit kernel-render node lang: "%s"' % (srclang)) |
db6ccf23 MH |
523 | |
524 | tmp_ext = RENDER_MARKUP_EXT.get(srclang, None) | |
525 | if tmp_ext is None: | |
096ea522 | 526 | kernellog.warn(app, 'kernel-render: "%s" unknown / include raw.' % (srclang)) |
db6ccf23 MH |
527 | return |
528 | ||
529 | if not dot_cmd and tmp_ext == '.dot': | |
096ea522 | 530 | kernellog.verbose(app, "dot from graphviz not available / include raw.") |
db6ccf23 MH |
531 | return |
532 | ||
533 | literal_block = node[0] | |
534 | ||
535 | code = literal_block.astext() | |
536 | hashobj = code.encode('utf-8') # str(node.attributes) | |
537 | fname = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest())) | |
538 | ||
539 | tmp_fname = path.join( | |
540 | self.builder.outdir, self.builder.imagedir, fname + tmp_ext) | |
541 | ||
542 | if not path.isfile(tmp_fname): | |
543 | mkdir(path.dirname(tmp_fname)) | |
544 | with open(tmp_fname, "w") as out: | |
545 | out.write(code) | |
546 | ||
547 | img_node = nodes.image(node.rawsource, **node.attributes) | |
548 | img_node['uri'] = path.join(self.builder.imgpath, fname + tmp_ext) | |
549 | img_node['candidates'] = { | |
550 | '*': path.join(self.builder.imgpath, fname + tmp_ext)} | |
551 | ||
552 | literal_block.replace_self(img_node) | |
553 | convert_image(img_node, self, tmp_fname) | |
554 | ||
555 | ||
556 | class kernel_render(nodes.General, nodes.Inline, nodes.Element): | |
557 | """Node for ``kernel-render`` directive.""" | |
558 | pass | |
559 | ||
560 | class KernelRender(Figure): | |
561 | u"""KernelRender directive | |
562 | ||
563 | Render content by external tool. Has all the options known from the | |
564 | *figure* directive, plus option ``caption``. If ``caption`` has a | |
565 | value, a figure node with the *caption* is inserted. If not, a image node is | |
566 | inserted. | |
567 | ||
568 | The KernelRender directive wraps the text of the directive into a | |
569 | literal_block node and wraps it into a kernel_render node. See | |
570 | ``visit_kernel_render``. | |
571 | """ | |
572 | has_content = True | |
573 | required_arguments = 1 | |
574 | optional_arguments = 0 | |
575 | final_argument_whitespace = False | |
576 | ||
577 | # earn options from 'figure' | |
578 | option_spec = Figure.option_spec.copy() | |
579 | option_spec['caption'] = directives.unchanged | |
580 | ||
581 | def run(self): | |
582 | return [self.build_node()] | |
583 | ||
584 | def build_node(self): | |
585 | ||
586 | srclang = self.arguments[0].strip() | |
587 | if srclang not in RENDER_MARKUP_EXT.keys(): | |
588 | return [self.state_machine.reporter.warning( | |
ae17a87d | 589 | 'Unknown source language "%s", use one of: %s.' % ( |
db6ccf23 MH |
590 | srclang, ",".join(RENDER_MARKUP_EXT.keys())), |
591 | line=self.lineno)] | |
592 | ||
593 | code = '\n'.join(self.content) | |
594 | if not code.strip(): | |
595 | return [self.state_machine.reporter.warning( | |
596 | 'Ignoring "%s" directive without content.' % ( | |
597 | self.name), | |
598 | line=self.lineno)] | |
599 | ||
600 | node = kernel_render() | |
601 | node['alt'] = self.options.get('alt','') | |
602 | node['srclang'] = srclang | |
603 | literal_node = nodes.literal_block(code, code) | |
604 | node += literal_node | |
605 | ||
606 | caption = self.options.get('caption') | |
607 | if caption: | |
608 | # parse caption's content | |
609 | parsed = nodes.Element() | |
610 | self.state.nested_parse( | |
611 | ViewList([caption], source=''), self.content_offset, parsed) | |
612 | caption_node = nodes.caption( | |
613 | parsed[0].rawsource, '', *parsed[0].children) | |
614 | caption_node.source = parsed[0].source | |
615 | caption_node.line = parsed[0].line | |
616 | ||
617 | figure_node = nodes.figure('', node) | |
618 | for k,v in self.options.items(): | |
619 | figure_node[k] = v | |
620 | figure_node += caption_node | |
621 | ||
622 | node = figure_node | |
623 | ||
624 | return node | |
625 | ||
626 | def add_kernel_figure_to_std_domain(app, doctree): | |
627 | """Add kernel-figure anchors to 'std' domain. | |
628 | ||
629 | The ``StandardDomain.process_doc(..)`` method does not know how to resolve | |
630 | the caption (label) of ``kernel-figure`` directive (it only knows about | |
631 | standard nodes, e.g. table, figure etc.). Without any additional handling | |
632 | this will result in a 'undefined label' for kernel-figures. | |
633 | ||
634 | This handle adds labels of kernel-figure to the 'std' domain labels. | |
635 | """ | |
636 | ||
637 | std = app.env.domains["std"] | |
638 | docname = app.env.docname | |
639 | labels = std.data["labels"] | |
640 | ||
4217e507 | 641 | for name, explicit in doctree.nametypes.items(): |
db6ccf23 MH |
642 | if not explicit: |
643 | continue | |
644 | labelid = doctree.nameids[name] | |
645 | if labelid is None: | |
646 | continue | |
647 | node = doctree.ids[labelid] | |
648 | ||
649 | if node.tagname == 'kernel_figure': | |
650 | for n in node.next_node(): | |
651 | if n.tagname == 'caption': | |
652 | sectname = clean_astext(n) | |
653 | # add label to std domain | |
654 | labels[name] = docname, labelid, sectname | |
655 | break |