kfigure.py 23 KB

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