Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 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 | |
Alexander A. Klimov | 93431e0 | 2020-05-26 08:05:44 +0200 | [diff] [blame] | 32 | * ``dot(1)``: Graphviz (https://www.graphviz.org). If Graphviz is not |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 33 | available, the DOT language is inserted as literal-block. |
| 34 | |
| 35 | * SVG to PDF: To generate PDF, you need at least one of this tools: |
| 36 | |
| 37 | - ``convert(1)``: ImageMagick (https://www.imagemagick.org) |
| 38 | |
| 39 | List of customizations: |
| 40 | |
| 41 | * generate PDF from SVG / used by PDF (LaTeX) builder |
| 42 | |
| 43 | * generate SVG (html-builder) and PDF (latex-builder) from DOT files. |
Alexander A. Klimov | 93431e0 | 2020-05-26 08:05:44 +0200 | [diff] [blame] | 44 | DOT: see https://www.graphviz.org/content/dot-language |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 45 | |
| 46 | """ |
| 47 | |
| 48 | import os |
| 49 | from os import path |
| 50 | import subprocess |
| 51 | from hashlib import sha1 |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 52 | from docutils import nodes |
| 53 | from docutils.statemachine import ViewList |
| 54 | from docutils.parsers.rst import directives |
| 55 | from docutils.parsers.rst.directives import images |
| 56 | import sphinx |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 57 | from sphinx.util.nodes import clean_astext |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 58 | import kernellog |
| 59 | |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 60 | # Get Sphinx version |
| 61 | major, minor, patch = sphinx.version_info[:3] |
| 62 | if major == 1 and minor > 3: |
| 63 | # patches.Figure only landed in Sphinx 1.4 |
| 64 | from sphinx.directives.patches import Figure # pylint: disable=C0413 |
| 65 | else: |
| 66 | Figure = images.Figure |
| 67 | |
| 68 | __version__ = '1.0.0' |
| 69 | |
| 70 | # simple helper |
| 71 | # ------------- |
| 72 | |
| 73 | def which(cmd): |
Masanari Iida | ae17a87 | 2018-01-11 20:00:28 +0900 | [diff] [blame] | 74 | """Searches the ``cmd`` in the ``PATH`` environment. |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 75 | |
| 76 | This *which* searches the PATH for executable ``cmd`` . First match is |
| 77 | returned, if nothing is found, ``None` is returned. |
| 78 | """ |
| 79 | envpath = os.environ.get('PATH', None) or os.defpath |
| 80 | for folder in envpath.split(os.pathsep): |
| 81 | fname = folder + os.sep + cmd |
| 82 | if path.isfile(fname): |
| 83 | return fname |
| 84 | |
| 85 | def mkdir(folder, mode=0o775): |
| 86 | if not path.isdir(folder): |
| 87 | os.makedirs(folder, mode) |
| 88 | |
| 89 | def file2literal(fname): |
| 90 | with open(fname, "r") as src: |
| 91 | data = src.read() |
| 92 | node = nodes.literal_block(data, data) |
| 93 | return node |
| 94 | |
| 95 | def isNewer(path1, path2): |
| 96 | """Returns True if ``path1`` is newer than ``path2`` |
| 97 | |
| 98 | If ``path1`` exists and is newer than ``path2`` the function returns |
| 99 | ``True`` is returned otherwise ``False`` |
| 100 | """ |
| 101 | return (path.exists(path1) |
| 102 | and os.stat(path1).st_ctime > os.stat(path2).st_ctime) |
| 103 | |
| 104 | def pass_handle(self, node): # pylint: disable=W0613 |
| 105 | pass |
| 106 | |
| 107 | # setup conversion tools and sphinx extension |
| 108 | # ------------------------------------------- |
| 109 | |
| 110 | # Graphviz's dot(1) support |
| 111 | dot_cmd = None |
| 112 | |
| 113 | # ImageMagick' convert(1) support |
| 114 | convert_cmd = None |
| 115 | |
| 116 | |
| 117 | def setup(app): |
| 118 | # check toolchain first |
| 119 | app.connect('builder-inited', setupTools) |
| 120 | |
| 121 | # image handling |
| 122 | app.add_directive("kernel-image", KernelImage) |
| 123 | app.add_node(kernel_image, |
| 124 | html = (visit_kernel_image, pass_handle), |
| 125 | latex = (visit_kernel_image, pass_handle), |
| 126 | texinfo = (visit_kernel_image, pass_handle), |
| 127 | text = (visit_kernel_image, pass_handle), |
| 128 | man = (visit_kernel_image, pass_handle), ) |
| 129 | |
| 130 | # figure handling |
| 131 | app.add_directive("kernel-figure", KernelFigure) |
| 132 | app.add_node(kernel_figure, |
| 133 | html = (visit_kernel_figure, pass_handle), |
| 134 | latex = (visit_kernel_figure, pass_handle), |
| 135 | texinfo = (visit_kernel_figure, pass_handle), |
| 136 | text = (visit_kernel_figure, pass_handle), |
| 137 | man = (visit_kernel_figure, pass_handle), ) |
| 138 | |
| 139 | # render handling |
| 140 | app.add_directive('kernel-render', KernelRender) |
| 141 | app.add_node(kernel_render, |
| 142 | html = (visit_kernel_render, pass_handle), |
| 143 | latex = (visit_kernel_render, pass_handle), |
| 144 | texinfo = (visit_kernel_render, pass_handle), |
| 145 | text = (visit_kernel_render, pass_handle), |
| 146 | man = (visit_kernel_render, pass_handle), ) |
| 147 | |
| 148 | app.connect('doctree-read', add_kernel_figure_to_std_domain) |
| 149 | |
| 150 | return dict( |
| 151 | version = __version__, |
| 152 | parallel_read_safe = True, |
| 153 | parallel_write_safe = True |
| 154 | ) |
| 155 | |
| 156 | |
| 157 | def setupTools(app): |
| 158 | u""" |
| 159 | Check available build tools and log some *verbose* messages. |
| 160 | |
| 161 | This function is called once, when the builder is initiated. |
| 162 | """ |
| 163 | global dot_cmd, convert_cmd # pylint: disable=W0603 |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 164 | kernellog.verbose(app, "kfigure: check installed tools ...") |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 165 | |
| 166 | dot_cmd = which('dot') |
| 167 | convert_cmd = which('convert') |
| 168 | |
| 169 | if dot_cmd: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 170 | kernellog.verbose(app, "use dot(1) from: " + dot_cmd) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 171 | else: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 172 | kernellog.warn(app, "dot(1) not found, for better output quality install " |
Alexander A. Klimov | 93431e0 | 2020-05-26 08:05:44 +0200 | [diff] [blame] | 173 | "graphviz from https://www.graphviz.org") |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 174 | if convert_cmd: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 175 | kernellog.verbose(app, "use convert(1) from: " + convert_cmd) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 176 | else: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 177 | kernellog.warn(app, |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 178 | "convert(1) not found, for SVG to PDF conversion install " |
| 179 | "ImageMagick (https://www.imagemagick.org)") |
| 180 | |
| 181 | |
| 182 | # integrate conversion tools |
| 183 | # -------------------------- |
| 184 | |
| 185 | RENDER_MARKUP_EXT = { |
| 186 | # The '.ext' must be handled by convert_image(..) function's *in_ext* input. |
| 187 | # <name> : <.ext> |
| 188 | 'DOT' : '.dot', |
| 189 | 'SVG' : '.svg' |
| 190 | } |
| 191 | |
| 192 | def convert_image(img_node, translator, src_fname=None): |
| 193 | """Convert a image node for the builder. |
| 194 | |
| 195 | Different builder prefer different image formats, e.g. *latex* builder |
| 196 | prefer PDF while *html* builder prefer SVG format for images. |
| 197 | |
| 198 | This function handles output image formats in dependence of source the |
| 199 | format (of the image) and the translator's output format. |
| 200 | """ |
| 201 | app = translator.builder.app |
| 202 | |
| 203 | fname, in_ext = path.splitext(path.basename(img_node['uri'])) |
| 204 | if src_fname is None: |
| 205 | src_fname = path.join(translator.builder.srcdir, img_node['uri']) |
| 206 | if not path.exists(src_fname): |
| 207 | src_fname = path.join(translator.builder.outdir, img_node['uri']) |
| 208 | |
| 209 | dst_fname = None |
| 210 | |
| 211 | # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages |
| 212 | |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 213 | kernellog.verbose(app, 'assert best format for: ' + img_node['uri']) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 214 | |
| 215 | if in_ext == '.dot': |
| 216 | |
| 217 | if not dot_cmd: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 218 | kernellog.verbose(app, |
| 219 | "dot from graphviz not available / include DOT raw.") |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 220 | img_node.replace_self(file2literal(src_fname)) |
| 221 | |
| 222 | elif translator.builder.format == 'latex': |
| 223 | dst_fname = path.join(translator.builder.outdir, fname + '.pdf') |
| 224 | img_node['uri'] = fname + '.pdf' |
| 225 | img_node['candidates'] = {'*': fname + '.pdf'} |
| 226 | |
| 227 | |
| 228 | elif translator.builder.format == 'html': |
| 229 | dst_fname = path.join( |
| 230 | translator.builder.outdir, |
| 231 | translator.builder.imagedir, |
| 232 | fname + '.svg') |
| 233 | img_node['uri'] = path.join( |
| 234 | translator.builder.imgpath, fname + '.svg') |
| 235 | img_node['candidates'] = { |
| 236 | '*': path.join(translator.builder.imgpath, fname + '.svg')} |
| 237 | |
| 238 | else: |
| 239 | # all other builder formats will include DOT as raw |
| 240 | img_node.replace_self(file2literal(src_fname)) |
| 241 | |
| 242 | elif in_ext == '.svg': |
| 243 | |
| 244 | if translator.builder.format == 'latex': |
| 245 | if convert_cmd is None: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 246 | kernellog.verbose(app, |
| 247 | "no SVG to PDF conversion available / include SVG raw.") |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 248 | img_node.replace_self(file2literal(src_fname)) |
| 249 | else: |
| 250 | dst_fname = path.join(translator.builder.outdir, fname + '.pdf') |
| 251 | img_node['uri'] = fname + '.pdf' |
| 252 | img_node['candidates'] = {'*': fname + '.pdf'} |
| 253 | |
| 254 | if dst_fname: |
| 255 | # the builder needs not to copy one more time, so pop it if exists. |
| 256 | translator.builder.images.pop(img_node['uri'], None) |
| 257 | _name = dst_fname[len(translator.builder.outdir) + 1:] |
| 258 | |
| 259 | if isNewer(dst_fname, src_fname): |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 260 | kernellog.verbose(app, |
| 261 | "convert: {out}/%s already exists and is newer" % _name) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 262 | |
| 263 | else: |
| 264 | ok = False |
| 265 | mkdir(path.dirname(dst_fname)) |
| 266 | |
| 267 | if in_ext == '.dot': |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 268 | kernellog.verbose(app, 'convert DOT to: {out}/' + _name) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 269 | ok = dot2format(app, src_fname, dst_fname) |
| 270 | |
| 271 | elif in_ext == '.svg': |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 272 | kernellog.verbose(app, 'convert SVG to: {out}/' + _name) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 273 | ok = svg2pdf(app, src_fname, dst_fname) |
| 274 | |
| 275 | if not ok: |
| 276 | img_node.replace_self(file2literal(src_fname)) |
| 277 | |
| 278 | |
| 279 | def dot2format(app, dot_fname, out_fname): |
| 280 | """Converts DOT file to ``out_fname`` using ``dot(1)``. |
| 281 | |
| 282 | * ``dot_fname`` pathname of the input DOT file, including extension ``.dot`` |
| 283 | * ``out_fname`` pathname of the output file, including format extension |
| 284 | |
| 285 | The *format extension* depends on the ``dot`` command (see ``man dot`` |
| 286 | option ``-Txxx``). Normally you will use one of the following extensions: |
| 287 | |
| 288 | - ``.ps`` for PostScript, |
| 289 | - ``.svg`` or ``svgz`` for Structured Vector Graphics, |
| 290 | - ``.fig`` for XFIG graphics and |
| 291 | - ``.png`` or ``gif`` for common bitmap graphics. |
| 292 | |
| 293 | """ |
| 294 | out_format = path.splitext(out_fname)[1][1:] |
| 295 | cmd = [dot_cmd, '-T%s' % out_format, dot_fname] |
| 296 | exit_code = 42 |
| 297 | |
| 298 | with open(out_fname, "w") as out: |
| 299 | exit_code = subprocess.call(cmd, stdout = out) |
| 300 | if exit_code != 0: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 301 | kernellog.warn(app, |
| 302 | "Error #%d when calling: %s" % (exit_code, " ".join(cmd))) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 303 | return bool(exit_code == 0) |
| 304 | |
| 305 | def svg2pdf(app, svg_fname, pdf_fname): |
| 306 | """Converts SVG to PDF with ``convert(1)`` command. |
| 307 | |
| 308 | Uses ``convert(1)`` from ImageMagick (https://www.imagemagick.org) for |
| 309 | conversion. Returns ``True`` on success and ``False`` if an error occurred. |
| 310 | |
| 311 | * ``svg_fname`` pathname of the input SVG file with extension (``.svg``) |
| 312 | * ``pdf_name`` pathname of the output PDF file with extension (``.pdf``) |
| 313 | |
| 314 | """ |
| 315 | cmd = [convert_cmd, svg_fname, pdf_fname] |
| 316 | # use stdout and stderr from parent |
| 317 | exit_code = subprocess.call(cmd) |
| 318 | if exit_code != 0: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 319 | kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd))) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 320 | return bool(exit_code == 0) |
| 321 | |
| 322 | |
| 323 | # image handling |
| 324 | # --------------------- |
| 325 | |
| 326 | def visit_kernel_image(self, node): # pylint: disable=W0613 |
| 327 | """Visitor of the ``kernel_image`` Node. |
| 328 | |
| 329 | Handles the ``image`` child-node with the ``convert_image(...)``. |
| 330 | """ |
| 331 | img_node = node[0] |
| 332 | convert_image(img_node, self) |
| 333 | |
| 334 | class kernel_image(nodes.image): |
| 335 | """Node for ``kernel-image`` directive.""" |
| 336 | pass |
| 337 | |
| 338 | class KernelImage(images.Image): |
| 339 | u"""KernelImage directive |
| 340 | |
| 341 | Earns everything from ``.. image::`` directive, except *remote URI* and |
| 342 | *glob* pattern. The KernelImage wraps a image node into a |
| 343 | kernel_image node. See ``visit_kernel_image``. |
| 344 | """ |
| 345 | |
| 346 | def run(self): |
| 347 | uri = self.arguments[0] |
| 348 | if uri.endswith('.*') or uri.find('://') != -1: |
| 349 | raise self.severe( |
| 350 | 'Error in "%s: %s": glob pattern and remote images are not allowed' |
| 351 | % (self.name, uri)) |
| 352 | result = images.Image.run(self) |
| 353 | if len(result) == 2 or isinstance(result[0], nodes.system_message): |
| 354 | return result |
| 355 | (image_node,) = result |
| 356 | # wrap image node into a kernel_image node / see visitors |
| 357 | node = kernel_image('', image_node) |
| 358 | return [node] |
| 359 | |
| 360 | # figure handling |
| 361 | # --------------------- |
| 362 | |
| 363 | def visit_kernel_figure(self, node): # pylint: disable=W0613 |
| 364 | """Visitor of the ``kernel_figure`` Node. |
| 365 | |
| 366 | Handles the ``image`` child-node with the ``convert_image(...)``. |
| 367 | """ |
| 368 | img_node = node[0][0] |
| 369 | convert_image(img_node, self) |
| 370 | |
| 371 | class kernel_figure(nodes.figure): |
| 372 | """Node for ``kernel-figure`` directive.""" |
| 373 | |
| 374 | class KernelFigure(Figure): |
| 375 | u"""KernelImage directive |
| 376 | |
| 377 | Earns everything from ``.. figure::`` directive, except *remote URI* and |
| 378 | *glob* pattern. The KernelFigure wraps a figure node into a kernel_figure |
| 379 | node. See ``visit_kernel_figure``. |
| 380 | """ |
| 381 | |
| 382 | def run(self): |
| 383 | uri = self.arguments[0] |
| 384 | if uri.endswith('.*') or uri.find('://') != -1: |
| 385 | raise self.severe( |
| 386 | 'Error in "%s: %s":' |
| 387 | ' glob pattern and remote images are not allowed' |
| 388 | % (self.name, uri)) |
| 389 | result = Figure.run(self) |
| 390 | if len(result) == 2 or isinstance(result[0], nodes.system_message): |
| 391 | return result |
| 392 | (figure_node,) = result |
| 393 | # wrap figure node into a kernel_figure node / see visitors |
| 394 | node = kernel_figure('', figure_node) |
| 395 | return [node] |
| 396 | |
| 397 | |
| 398 | # render handling |
| 399 | # --------------------- |
| 400 | |
| 401 | def visit_kernel_render(self, node): |
| 402 | """Visitor of the ``kernel_render`` Node. |
| 403 | |
| 404 | If rendering tools available, save the markup of the ``literal_block`` child |
| 405 | node into a file and replace the ``literal_block`` node with a new created |
| 406 | ``image`` node, pointing to the saved markup file. Afterwards, handle the |
| 407 | image child-node with the ``convert_image(...)``. |
| 408 | """ |
| 409 | app = self.builder.app |
| 410 | srclang = node.get('srclang') |
| 411 | |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 412 | kernellog.verbose(app, 'visit kernel-render node lang: "%s"' % (srclang)) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 413 | |
| 414 | tmp_ext = RENDER_MARKUP_EXT.get(srclang, None) |
| 415 | if tmp_ext is None: |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 416 | kernellog.warn(app, 'kernel-render: "%s" unknown / include raw.' % (srclang)) |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 417 | return |
| 418 | |
| 419 | if not dot_cmd and tmp_ext == '.dot': |
Jonathan Corbet | 096ea52 | 2019-05-21 14:23:43 -0600 | [diff] [blame] | 420 | kernellog.verbose(app, "dot from graphviz not available / include raw.") |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 421 | return |
| 422 | |
| 423 | literal_block = node[0] |
| 424 | |
| 425 | code = literal_block.astext() |
| 426 | hashobj = code.encode('utf-8') # str(node.attributes) |
| 427 | fname = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest())) |
| 428 | |
| 429 | tmp_fname = path.join( |
| 430 | self.builder.outdir, self.builder.imagedir, fname + tmp_ext) |
| 431 | |
| 432 | if not path.isfile(tmp_fname): |
| 433 | mkdir(path.dirname(tmp_fname)) |
| 434 | with open(tmp_fname, "w") as out: |
| 435 | out.write(code) |
| 436 | |
| 437 | img_node = nodes.image(node.rawsource, **node.attributes) |
| 438 | img_node['uri'] = path.join(self.builder.imgpath, fname + tmp_ext) |
| 439 | img_node['candidates'] = { |
| 440 | '*': path.join(self.builder.imgpath, fname + tmp_ext)} |
| 441 | |
| 442 | literal_block.replace_self(img_node) |
| 443 | convert_image(img_node, self, tmp_fname) |
| 444 | |
| 445 | |
| 446 | class kernel_render(nodes.General, nodes.Inline, nodes.Element): |
| 447 | """Node for ``kernel-render`` directive.""" |
| 448 | pass |
| 449 | |
| 450 | class KernelRender(Figure): |
| 451 | u"""KernelRender directive |
| 452 | |
| 453 | Render content by external tool. Has all the options known from the |
| 454 | *figure* directive, plus option ``caption``. If ``caption`` has a |
| 455 | value, a figure node with the *caption* is inserted. If not, a image node is |
| 456 | inserted. |
| 457 | |
| 458 | The KernelRender directive wraps the text of the directive into a |
| 459 | literal_block node and wraps it into a kernel_render node. See |
| 460 | ``visit_kernel_render``. |
| 461 | """ |
| 462 | has_content = True |
| 463 | required_arguments = 1 |
| 464 | optional_arguments = 0 |
| 465 | final_argument_whitespace = False |
| 466 | |
| 467 | # earn options from 'figure' |
| 468 | option_spec = Figure.option_spec.copy() |
| 469 | option_spec['caption'] = directives.unchanged |
| 470 | |
| 471 | def run(self): |
| 472 | return [self.build_node()] |
| 473 | |
| 474 | def build_node(self): |
| 475 | |
| 476 | srclang = self.arguments[0].strip() |
| 477 | if srclang not in RENDER_MARKUP_EXT.keys(): |
| 478 | return [self.state_machine.reporter.warning( |
Masanari Iida | ae17a87 | 2018-01-11 20:00:28 +0900 | [diff] [blame] | 479 | 'Unknown source language "%s", use one of: %s.' % ( |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 480 | srclang, ",".join(RENDER_MARKUP_EXT.keys())), |
| 481 | line=self.lineno)] |
| 482 | |
| 483 | code = '\n'.join(self.content) |
| 484 | if not code.strip(): |
| 485 | return [self.state_machine.reporter.warning( |
| 486 | 'Ignoring "%s" directive without content.' % ( |
| 487 | self.name), |
| 488 | line=self.lineno)] |
| 489 | |
| 490 | node = kernel_render() |
| 491 | node['alt'] = self.options.get('alt','') |
| 492 | node['srclang'] = srclang |
| 493 | literal_node = nodes.literal_block(code, code) |
| 494 | node += literal_node |
| 495 | |
| 496 | caption = self.options.get('caption') |
| 497 | if caption: |
| 498 | # parse caption's content |
| 499 | parsed = nodes.Element() |
| 500 | self.state.nested_parse( |
| 501 | ViewList([caption], source=''), self.content_offset, parsed) |
| 502 | caption_node = nodes.caption( |
| 503 | parsed[0].rawsource, '', *parsed[0].children) |
| 504 | caption_node.source = parsed[0].source |
| 505 | caption_node.line = parsed[0].line |
| 506 | |
| 507 | figure_node = nodes.figure('', node) |
| 508 | for k,v in self.options.items(): |
| 509 | figure_node[k] = v |
| 510 | figure_node += caption_node |
| 511 | |
| 512 | node = figure_node |
| 513 | |
| 514 | return node |
| 515 | |
| 516 | def add_kernel_figure_to_std_domain(app, doctree): |
| 517 | """Add kernel-figure anchors to 'std' domain. |
| 518 | |
| 519 | The ``StandardDomain.process_doc(..)`` method does not know how to resolve |
| 520 | the caption (label) of ``kernel-figure`` directive (it only knows about |
| 521 | standard nodes, e.g. table, figure etc.). Without any additional handling |
| 522 | this will result in a 'undefined label' for kernel-figures. |
| 523 | |
| 524 | This handle adds labels of kernel-figure to the 'std' domain labels. |
| 525 | """ |
| 526 | |
| 527 | std = app.env.domains["std"] |
| 528 | docname = app.env.docname |
| 529 | labels = std.data["labels"] |
| 530 | |
Jonathan Corbet | 4217e50 | 2021-02-01 17:17:14 -0700 | [diff] [blame] | 531 | for name, explicit in doctree.nametypes.items(): |
Markus Heiser | db6ccf2 | 2017-03-06 14:09:27 +0100 | [diff] [blame] | 532 | if not explicit: |
| 533 | continue |
| 534 | labelid = doctree.nameids[name] |
| 535 | if labelid is None: |
| 536 | continue |
| 537 | node = doctree.ids[labelid] |
| 538 | |
| 539 | if node.tagname == 'kernel_figure': |
| 540 | for n in node.next_node(): |
| 541 | if n.tagname == 'caption': |
| 542 | sectname = clean_astext(n) |
| 543 | # add label to std domain |
| 544 | labels[name] = docname, labelid, sectname |
| 545 | break |