Back to home page

OSCL-LXR

 
 

    


0001 # -*- coding: utf-8; mode: python -*-
0002 # pylint: disable=C0103, R0903, R0912, R0915
0003 u"""
0004     scalable figure and image handling
0005     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0006 
0007     Sphinx extension which implements scalable image handling.
0008 
0009     :copyright:  Copyright (C) 2016  Markus Heiser
0010     :license:    GPL Version 2, June 1991 see Linux/COPYING for details.
0011 
0012     The build for image formats depend on image's source format and output's
0013     destination format. This extension implement methods to simplify image
0014     handling from the author's POV. Directives like ``kernel-figure`` implement
0015     methods *to* always get the best output-format even if some tools are not
0016     installed. For more details take a look at ``convert_image(...)`` which is
0017     the core of all conversions.
0018 
0019     * ``.. kernel-image``: for image handling / a ``.. image::`` replacement
0020 
0021     * ``.. kernel-figure``: for figure handling / a ``.. figure::`` replacement
0022 
0023     * ``.. kernel-render``: for render markup / a concept to embed *render*
0024       markups (or languages). Supported markups (see ``RENDER_MARKUP_EXT``)
0025 
0026       - ``DOT``: render embedded Graphviz's **DOC**
0027       - ``SVG``: render embedded Scalable Vector Graphics (**SVG**)
0028       - ... *developable*
0029 
0030     Used tools:
0031 
0032     * ``dot(1)``: Graphviz (https://www.graphviz.org). If Graphviz is not
0033       available, the DOT language is inserted as literal-block.
0034       For conversion to PDF, ``rsvg-convert(1)`` of librsvg
0035       (https://gitlab.gnome.org/GNOME/librsvg) is used when available.
0036 
0037     * SVG to PDF: To generate PDF, you need at least one of this tools:
0038 
0039       - ``convert(1)``: ImageMagick (https://www.imagemagick.org)
0040       - ``inkscape(1)``: Inkscape (https://inkscape.org/)
0041 
0042     List of customizations:
0043 
0044     * generate PDF from SVG / used by PDF (LaTeX) builder
0045 
0046     * generate SVG (html-builder) and PDF (latex-builder) from DOT files.
0047       DOT: see https://www.graphviz.org/content/dot-language
0048 
0049     """
0050 
0051 import os
0052 from os import path
0053 import subprocess
0054 from hashlib import sha1
0055 import re
0056 from docutils import nodes
0057 from docutils.statemachine import ViewList
0058 from docutils.parsers.rst import directives
0059 from docutils.parsers.rst.directives import images
0060 import sphinx
0061 from sphinx.util.nodes import clean_astext
0062 import kernellog
0063 
0064 # Get Sphinx version
0065 major, minor, patch = sphinx.version_info[:3]
0066 if major == 1 and minor > 3:
0067     # patches.Figure only landed in Sphinx 1.4
0068     from sphinx.directives.patches import Figure  # pylint: disable=C0413
0069 else:
0070     Figure = images.Figure
0071 
0072 __version__  = '1.0.0'
0073 
0074 # simple helper
0075 # -------------
0076 
0077 def which(cmd):
0078     """Searches the ``cmd`` in the ``PATH`` environment.
0079 
0080     This *which* searches the PATH for executable ``cmd`` . First match is
0081     returned, if nothing is found, ``None` is returned.
0082     """
0083     envpath = os.environ.get('PATH', None) or os.defpath
0084     for folder in envpath.split(os.pathsep):
0085         fname = folder + os.sep + cmd
0086         if path.isfile(fname):
0087             return fname
0088 
0089 def mkdir(folder, mode=0o775):
0090     if not path.isdir(folder):
0091         os.makedirs(folder, mode)
0092 
0093 def file2literal(fname):
0094     with open(fname, "r") as src:
0095         data = src.read()
0096         node = nodes.literal_block(data, data)
0097     return node
0098 
0099 def isNewer(path1, path2):
0100     """Returns True if ``path1`` is newer than ``path2``
0101 
0102     If ``path1`` exists and is newer than ``path2`` the function returns
0103     ``True`` is returned otherwise ``False``
0104     """
0105     return (path.exists(path1)
0106             and os.stat(path1).st_ctime > os.stat(path2).st_ctime)
0107 
0108 def pass_handle(self, node):           # pylint: disable=W0613
0109     pass
0110 
0111 # setup conversion tools and sphinx extension
0112 # -------------------------------------------
0113 
0114 # Graphviz's dot(1) support
0115 dot_cmd = None
0116 # dot(1) -Tpdf should be used
0117 dot_Tpdf = False
0118 
0119 # ImageMagick' convert(1) support
0120 convert_cmd = None
0121 
0122 # librsvg's rsvg-convert(1) support
0123 rsvg_convert_cmd = None
0124 
0125 # Inkscape's inkscape(1) support
0126 inkscape_cmd = None
0127 # Inkscape prior to 1.0 uses different command options
0128 inkscape_ver_one = False
0129 
0130 
0131 def setup(app):
0132     # check toolchain first
0133     app.connect('builder-inited', setupTools)
0134 
0135     # image handling
0136     app.add_directive("kernel-image",  KernelImage)
0137     app.add_node(kernel_image,
0138                  html    = (visit_kernel_image, pass_handle),
0139                  latex   = (visit_kernel_image, pass_handle),
0140                  texinfo = (visit_kernel_image, pass_handle),
0141                  text    = (visit_kernel_image, pass_handle),
0142                  man     = (visit_kernel_image, pass_handle), )
0143 
0144     # figure handling
0145     app.add_directive("kernel-figure", KernelFigure)
0146     app.add_node(kernel_figure,
0147                  html    = (visit_kernel_figure, pass_handle),
0148                  latex   = (visit_kernel_figure, pass_handle),
0149                  texinfo = (visit_kernel_figure, pass_handle),
0150                  text    = (visit_kernel_figure, pass_handle),
0151                  man     = (visit_kernel_figure, pass_handle), )
0152 
0153     # render handling
0154     app.add_directive('kernel-render', KernelRender)
0155     app.add_node(kernel_render,
0156                  html    = (visit_kernel_render, pass_handle),
0157                  latex   = (visit_kernel_render, pass_handle),
0158                  texinfo = (visit_kernel_render, pass_handle),
0159                  text    = (visit_kernel_render, pass_handle),
0160                  man     = (visit_kernel_render, pass_handle), )
0161 
0162     app.connect('doctree-read', add_kernel_figure_to_std_domain)
0163 
0164     return dict(
0165         version = __version__,
0166         parallel_read_safe = True,
0167         parallel_write_safe = True
0168     )
0169 
0170 
0171 def setupTools(app):
0172     u"""
0173     Check available build tools and log some *verbose* messages.
0174 
0175     This function is called once, when the builder is initiated.
0176     """
0177     global dot_cmd, dot_Tpdf, convert_cmd, rsvg_convert_cmd   # pylint: disable=W0603
0178     global inkscape_cmd, inkscape_ver_one  # pylint: disable=W0603
0179     kernellog.verbose(app, "kfigure: check installed tools ...")
0180 
0181     dot_cmd = which('dot')
0182     convert_cmd = which('convert')
0183     rsvg_convert_cmd = which('rsvg-convert')
0184     inkscape_cmd = which('inkscape')
0185 
0186     if dot_cmd:
0187         kernellog.verbose(app, "use dot(1) from: " + dot_cmd)
0188 
0189         try:
0190             dot_Thelp_list = subprocess.check_output([dot_cmd, '-Thelp'],
0191                                     stderr=subprocess.STDOUT)
0192         except subprocess.CalledProcessError as err:
0193             dot_Thelp_list = err.output
0194             pass
0195 
0196         dot_Tpdf_ptn = b'pdf'
0197         dot_Tpdf = re.search(dot_Tpdf_ptn, dot_Thelp_list)
0198     else:
0199         kernellog.warn(app, "dot(1) not found, for better output quality install "
0200                        "graphviz from https://www.graphviz.org")
0201     if inkscape_cmd:
0202         kernellog.verbose(app, "use inkscape(1) from: " + inkscape_cmd)
0203         inkscape_ver = subprocess.check_output([inkscape_cmd, '--version'],
0204                                                stderr=subprocess.DEVNULL)
0205         ver_one_ptn = b'Inkscape 1'
0206         inkscape_ver_one = re.search(ver_one_ptn, inkscape_ver)
0207         convert_cmd = None
0208         rsvg_convert_cmd = None
0209         dot_Tpdf = False
0210 
0211     else:
0212         if convert_cmd:
0213             kernellog.verbose(app, "use convert(1) from: " + convert_cmd)
0214         else:
0215             kernellog.verbose(app,
0216                 "Neither inkscape(1) nor convert(1) found.\n"
0217                 "For SVG to PDF conversion, "
0218                 "install either Inkscape (https://inkscape.org/) (preferred) or\n"
0219                 "ImageMagick (https://www.imagemagick.org)")
0220 
0221         if rsvg_convert_cmd:
0222             kernellog.verbose(app, "use rsvg-convert(1) from: " + rsvg_convert_cmd)
0223             kernellog.verbose(app, "use 'dot -Tsvg' and rsvg-convert(1) for DOT -> PDF conversion")
0224             dot_Tpdf = False
0225         else:
0226             kernellog.verbose(app,
0227                 "rsvg-convert(1) not found.\n"
0228                 "  SVG rendering of convert(1) is done by ImageMagick-native renderer.")
0229             if dot_Tpdf:
0230                 kernellog.verbose(app, "use 'dot -Tpdf' for DOT -> PDF conversion")
0231             else:
0232                 kernellog.verbose(app, "use 'dot -Tsvg' and convert(1) for DOT -> PDF conversion")
0233 
0234 
0235 # integrate conversion tools
0236 # --------------------------
0237 
0238 RENDER_MARKUP_EXT = {
0239     # The '.ext' must be handled by convert_image(..) function's *in_ext* input.
0240     # <name> : <.ext>
0241     'DOT' : '.dot',
0242     'SVG' : '.svg'
0243 }
0244 
0245 def convert_image(img_node, translator, src_fname=None):
0246     """Convert a image node for the builder.
0247 
0248     Different builder prefer different image formats, e.g. *latex* builder
0249     prefer PDF while *html* builder prefer SVG format for images.
0250 
0251     This function handles output image formats in dependence of source the
0252     format (of the image) and the translator's output format.
0253     """
0254     app = translator.builder.app
0255 
0256     fname, in_ext = path.splitext(path.basename(img_node['uri']))
0257     if src_fname is None:
0258         src_fname = path.join(translator.builder.srcdir, img_node['uri'])
0259         if not path.exists(src_fname):
0260             src_fname = path.join(translator.builder.outdir, img_node['uri'])
0261 
0262     dst_fname = None
0263 
0264     # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages
0265 
0266     kernellog.verbose(app, 'assert best format for: ' + img_node['uri'])
0267 
0268     if in_ext == '.dot':
0269 
0270         if not dot_cmd:
0271             kernellog.verbose(app,
0272                               "dot from graphviz not available / include DOT raw.")
0273             img_node.replace_self(file2literal(src_fname))
0274 
0275         elif translator.builder.format == 'latex':
0276             dst_fname = path.join(translator.builder.outdir, fname + '.pdf')
0277             img_node['uri'] = fname + '.pdf'
0278             img_node['candidates'] = {'*': fname + '.pdf'}
0279 
0280 
0281         elif translator.builder.format == 'html':
0282             dst_fname = path.join(
0283                 translator.builder.outdir,
0284                 translator.builder.imagedir,
0285                 fname + '.svg')
0286             img_node['uri'] = path.join(
0287                 translator.builder.imgpath, fname + '.svg')
0288             img_node['candidates'] = {
0289                 '*': path.join(translator.builder.imgpath, fname + '.svg')}
0290 
0291         else:
0292             # all other builder formats will include DOT as raw
0293             img_node.replace_self(file2literal(src_fname))
0294 
0295     elif in_ext == '.svg':
0296 
0297         if translator.builder.format == 'latex':
0298             if not inkscape_cmd and convert_cmd is None:
0299                 kernellog.warn(app,
0300                                   "no SVG to PDF conversion available / include SVG raw."
0301                                   "\nIncluding large raw SVGs can cause xelatex error."
0302                                   "\nInstall Inkscape (preferred) or ImageMagick.")
0303                 img_node.replace_self(file2literal(src_fname))
0304             else:
0305                 dst_fname = path.join(translator.builder.outdir, fname + '.pdf')
0306                 img_node['uri'] = fname + '.pdf'
0307                 img_node['candidates'] = {'*': fname + '.pdf'}
0308 
0309     if dst_fname:
0310         # the builder needs not to copy one more time, so pop it if exists.
0311         translator.builder.images.pop(img_node['uri'], None)
0312         _name = dst_fname[len(translator.builder.outdir) + 1:]
0313 
0314         if isNewer(dst_fname, src_fname):
0315             kernellog.verbose(app,
0316                               "convert: {out}/%s already exists and is newer" % _name)
0317 
0318         else:
0319             ok = False
0320             mkdir(path.dirname(dst_fname))
0321 
0322             if in_ext == '.dot':
0323                 kernellog.verbose(app, 'convert DOT to: {out}/' + _name)
0324                 if translator.builder.format == 'latex' and not dot_Tpdf:
0325                     svg_fname = path.join(translator.builder.outdir, fname + '.svg')
0326                     ok1 = dot2format(app, src_fname, svg_fname)
0327                     ok2 = svg2pdf_by_rsvg(app, svg_fname, dst_fname)
0328                     ok = ok1 and ok2
0329 
0330                 else:
0331                     ok = dot2format(app, src_fname, dst_fname)
0332 
0333             elif in_ext == '.svg':
0334                 kernellog.verbose(app, 'convert SVG to: {out}/' + _name)
0335                 ok = svg2pdf(app, src_fname, dst_fname)
0336 
0337             if not ok:
0338                 img_node.replace_self(file2literal(src_fname))
0339 
0340 
0341 def dot2format(app, dot_fname, out_fname):
0342     """Converts DOT file to ``out_fname`` using ``dot(1)``.
0343 
0344     * ``dot_fname`` pathname of the input DOT file, including extension ``.dot``
0345     * ``out_fname`` pathname of the output file, including format extension
0346 
0347     The *format extension* depends on the ``dot`` command (see ``man dot``
0348     option ``-Txxx``). Normally you will use one of the following extensions:
0349 
0350     - ``.ps`` for PostScript,
0351     - ``.svg`` or ``svgz`` for Structured Vector Graphics,
0352     - ``.fig`` for XFIG graphics and
0353     - ``.png`` or ``gif`` for common bitmap graphics.
0354 
0355     """
0356     out_format = path.splitext(out_fname)[1][1:]
0357     cmd = [dot_cmd, '-T%s' % out_format, dot_fname]
0358     exit_code = 42
0359 
0360     with open(out_fname, "w") as out:
0361         exit_code = subprocess.call(cmd, stdout = out)
0362         if exit_code != 0:
0363             kernellog.warn(app,
0364                           "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
0365     return bool(exit_code == 0)
0366 
0367 def svg2pdf(app, svg_fname, pdf_fname):
0368     """Converts SVG to PDF with ``inkscape(1)`` or ``convert(1)`` command.
0369 
0370     Uses ``inkscape(1)`` from Inkscape (https://inkscape.org/) or ``convert(1)``
0371     from ImageMagick (https://www.imagemagick.org) for conversion.
0372     Returns ``True`` on success and ``False`` if an error occurred.
0373 
0374     * ``svg_fname`` pathname of the input SVG file with extension (``.svg``)
0375     * ``pdf_name``  pathname of the output PDF file with extension (``.pdf``)
0376 
0377     """
0378     cmd = [convert_cmd, svg_fname, pdf_fname]
0379     cmd_name = 'convert(1)'
0380 
0381     if inkscape_cmd:
0382         cmd_name = 'inkscape(1)'
0383         if inkscape_ver_one:
0384             cmd = [inkscape_cmd, '-o', pdf_fname, svg_fname]
0385         else:
0386             cmd = [inkscape_cmd, '-z', '--export-pdf=%s' % pdf_fname, svg_fname]
0387 
0388     try:
0389         warning_msg = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
0390         exit_code = 0
0391     except subprocess.CalledProcessError as err:
0392         warning_msg = err.output
0393         exit_code = err.returncode
0394         pass
0395 
0396     if exit_code != 0:
0397         kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
0398         if warning_msg:
0399             kernellog.warn(app, "Warning msg from %s: %s"
0400                            % (cmd_name, str(warning_msg, 'utf-8')))
0401     elif warning_msg:
0402         kernellog.verbose(app, "Warning msg from %s (likely harmless):\n%s"
0403                           % (cmd_name, str(warning_msg, 'utf-8')))
0404 
0405     return bool(exit_code == 0)
0406 
0407 def svg2pdf_by_rsvg(app, svg_fname, pdf_fname):
0408     """Convert SVG to PDF with ``rsvg-convert(1)`` command.
0409 
0410     * ``svg_fname`` pathname of input SVG file, including extension ``.svg``
0411     * ``pdf_fname`` pathname of output PDF file, including extension ``.pdf``
0412 
0413     Input SVG file should be the one generated by ``dot2format()``.
0414     SVG -> PDF conversion is done by ``rsvg-convert(1)``.
0415 
0416     If ``rsvg-convert(1)`` is unavailable, fall back to ``svg2pdf()``.
0417 
0418     """
0419 
0420     if rsvg_convert_cmd is None:
0421         ok = svg2pdf(app, svg_fname, pdf_fname)
0422     else:
0423         cmd = [rsvg_convert_cmd, '--format=pdf', '-o', pdf_fname, svg_fname]
0424         # use stdout and stderr from parent
0425         exit_code = subprocess.call(cmd)
0426         if exit_code != 0:
0427             kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
0428         ok = bool(exit_code == 0)
0429 
0430     return ok
0431 
0432 
0433 # image handling
0434 # ---------------------
0435 
0436 def visit_kernel_image(self, node):    # pylint: disable=W0613
0437     """Visitor of the ``kernel_image`` Node.
0438 
0439     Handles the ``image`` child-node with the ``convert_image(...)``.
0440     """
0441     img_node = node[0]
0442     convert_image(img_node, self)
0443 
0444 class kernel_image(nodes.image):
0445     """Node for ``kernel-image`` directive."""
0446     pass
0447 
0448 class KernelImage(images.Image):
0449     u"""KernelImage directive
0450 
0451     Earns everything from ``.. image::`` directive, except *remote URI* and
0452     *glob* pattern. The KernelImage wraps a image node into a
0453     kernel_image node. See ``visit_kernel_image``.
0454     """
0455 
0456     def run(self):
0457         uri = self.arguments[0]
0458         if uri.endswith('.*') or uri.find('://') != -1:
0459             raise self.severe(
0460                 'Error in "%s: %s": glob pattern and remote images are not allowed'
0461                 % (self.name, uri))
0462         result = images.Image.run(self)
0463         if len(result) == 2 or isinstance(result[0], nodes.system_message):
0464             return result
0465         (image_node,) = result
0466         # wrap image node into a kernel_image node / see visitors
0467         node = kernel_image('', image_node)
0468         return [node]
0469 
0470 # figure handling
0471 # ---------------------
0472 
0473 def visit_kernel_figure(self, node):   # pylint: disable=W0613
0474     """Visitor of the ``kernel_figure`` Node.
0475 
0476     Handles the ``image`` child-node with the ``convert_image(...)``.
0477     """
0478     img_node = node[0][0]
0479     convert_image(img_node, self)
0480 
0481 class kernel_figure(nodes.figure):
0482     """Node for ``kernel-figure`` directive."""
0483 
0484 class KernelFigure(Figure):
0485     u"""KernelImage directive
0486 
0487     Earns everything from ``.. figure::`` directive, except *remote URI* and
0488     *glob* pattern.  The KernelFigure wraps a figure node into a kernel_figure
0489     node. See ``visit_kernel_figure``.
0490     """
0491 
0492     def run(self):
0493         uri = self.arguments[0]
0494         if uri.endswith('.*') or uri.find('://') != -1:
0495             raise self.severe(
0496                 'Error in "%s: %s":'
0497                 ' glob pattern and remote images are not allowed'
0498                 % (self.name, uri))
0499         result = Figure.run(self)
0500         if len(result) == 2 or isinstance(result[0], nodes.system_message):
0501             return result
0502         (figure_node,) = result
0503         # wrap figure node into a kernel_figure node / see visitors
0504         node = kernel_figure('', figure_node)
0505         return [node]
0506 
0507 
0508 # render handling
0509 # ---------------------
0510 
0511 def visit_kernel_render(self, node):
0512     """Visitor of the ``kernel_render`` Node.
0513 
0514     If rendering tools available, save the markup of the ``literal_block`` child
0515     node into a file and replace the ``literal_block`` node with a new created
0516     ``image`` node, pointing to the saved markup file. Afterwards, handle the
0517     image child-node with the ``convert_image(...)``.
0518     """
0519     app = self.builder.app
0520     srclang = node.get('srclang')
0521 
0522     kernellog.verbose(app, 'visit kernel-render node lang: "%s"' % (srclang))
0523 
0524     tmp_ext = RENDER_MARKUP_EXT.get(srclang, None)
0525     if tmp_ext is None:
0526         kernellog.warn(app, 'kernel-render: "%s" unknown / include raw.' % (srclang))
0527         return
0528 
0529     if not dot_cmd and tmp_ext == '.dot':
0530         kernellog.verbose(app, "dot from graphviz not available / include raw.")
0531         return
0532 
0533     literal_block = node[0]
0534 
0535     code      = literal_block.astext()
0536     hashobj   = code.encode('utf-8') #  str(node.attributes)
0537     fname     = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest()))
0538 
0539     tmp_fname = path.join(
0540         self.builder.outdir, self.builder.imagedir, fname + tmp_ext)
0541 
0542     if not path.isfile(tmp_fname):
0543         mkdir(path.dirname(tmp_fname))
0544         with open(tmp_fname, "w") as out:
0545             out.write(code)
0546 
0547     img_node = nodes.image(node.rawsource, **node.attributes)
0548     img_node['uri'] = path.join(self.builder.imgpath, fname + tmp_ext)
0549     img_node['candidates'] = {
0550         '*': path.join(self.builder.imgpath, fname + tmp_ext)}
0551 
0552     literal_block.replace_self(img_node)
0553     convert_image(img_node, self, tmp_fname)
0554 
0555 
0556 class kernel_render(nodes.General, nodes.Inline, nodes.Element):
0557     """Node for ``kernel-render`` directive."""
0558     pass
0559 
0560 class KernelRender(Figure):
0561     u"""KernelRender directive
0562 
0563     Render content by external tool.  Has all the options known from the
0564     *figure*  directive, plus option ``caption``.  If ``caption`` has a
0565     value, a figure node with the *caption* is inserted. If not, a image node is
0566     inserted.
0567 
0568     The KernelRender directive wraps the text of the directive into a
0569     literal_block node and wraps it into a kernel_render node. See
0570     ``visit_kernel_render``.
0571     """
0572     has_content = True
0573     required_arguments = 1
0574     optional_arguments = 0
0575     final_argument_whitespace = False
0576 
0577     # earn options from 'figure'
0578     option_spec = Figure.option_spec.copy()
0579     option_spec['caption'] = directives.unchanged
0580 
0581     def run(self):
0582         return [self.build_node()]
0583 
0584     def build_node(self):
0585 
0586         srclang = self.arguments[0].strip()
0587         if srclang not in RENDER_MARKUP_EXT.keys():
0588             return [self.state_machine.reporter.warning(
0589                 'Unknown source language "%s", use one of: %s.' % (
0590                     srclang, ",".join(RENDER_MARKUP_EXT.keys())),
0591                 line=self.lineno)]
0592 
0593         code = '\n'.join(self.content)
0594         if not code.strip():
0595             return [self.state_machine.reporter.warning(
0596                 'Ignoring "%s" directive without content.' % (
0597                     self.name),
0598                 line=self.lineno)]
0599 
0600         node = kernel_render()
0601         node['alt'] = self.options.get('alt','')
0602         node['srclang'] = srclang
0603         literal_node = nodes.literal_block(code, code)
0604         node += literal_node
0605 
0606         caption = self.options.get('caption')
0607         if caption:
0608             # parse caption's content
0609             parsed = nodes.Element()
0610             self.state.nested_parse(
0611                 ViewList([caption], source=''), self.content_offset, parsed)
0612             caption_node = nodes.caption(
0613                 parsed[0].rawsource, '', *parsed[0].children)
0614             caption_node.source = parsed[0].source
0615             caption_node.line = parsed[0].line
0616 
0617             figure_node = nodes.figure('', node)
0618             for k,v in self.options.items():
0619                 figure_node[k] = v
0620             figure_node += caption_node
0621 
0622             node = figure_node
0623 
0624         return node
0625 
0626 def add_kernel_figure_to_std_domain(app, doctree):
0627     """Add kernel-figure anchors to 'std' domain.
0628 
0629     The ``StandardDomain.process_doc(..)`` method does not know how to resolve
0630     the caption (label) of ``kernel-figure`` directive (it only knows about
0631     standard nodes, e.g. table, figure etc.). Without any additional handling
0632     this will result in a 'undefined label' for kernel-figures.
0633 
0634     This handle adds labels of kernel-figure to the 'std' domain labels.
0635     """
0636 
0637     std = app.env.domains["std"]
0638     docname = app.env.docname
0639     labels = std.data["labels"]
0640 
0641     for name, explicit in doctree.nametypes.items():
0642         if not explicit:
0643             continue
0644         labelid = doctree.nameids[name]
0645         if labelid is None:
0646             continue
0647         node = doctree.ids[labelid]
0648 
0649         if node.tagname == 'kernel_figure':
0650             for n in node.next_node():
0651                 if n.tagname == 'caption':
0652                     sectname = clean_astext(n)
0653                     # add label to std domain
0654                     labels[name] = docname, labelid, sectname
0655                     break