Source code for sphinx_gallery.directives

"""
Custom Sphinx directives
========================
"""
import os
from pathlib import PurePosixPath
import shutil

from docutils import nodes
from docutils import statemachine
from docutils.parsers.rst import Directive, directives
from docutils.parsers.rst.directives import images

from sphinx.errors import ExtensionError


[docs]class MiniGallery(Directive): """ Custom directive to insert a mini-gallery The required argument is one or more fully qualified names of objects, separated by spaces. The mini-gallery will be the subset of gallery examples that make use of that object (from that specific namespace). Options: * `add-heading` adds a heading to the mini-gallery. If an argument is provided, it uses that text for the heading. Otherwise, it uses default text. * `heading-level` specifies the heading level of the heading as a single character. If omitted, the default heading level is `'^'`. """ required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec = {'add-heading': directives.unchanged, 'heading-level': directives.single_char_or_unicode} def run(self): # Respect the same disabling options as the `raw` directive if (not self.state.document.settings.raw_enabled or not self.state.document.settings.file_insertion_enabled): raise self.warning('"%s" directive disabled.' % self.name) # Retrieve the backreferences directory config = self.state.document.settings.env.config backreferences_dir = config.sphinx_gallery_conf['backreferences_dir'] # Parse the argument into the individual objects obj_list = self.arguments[0].split() lines = [] # Add a heading if requested if 'add-heading' in self.options: heading = self.options['add-heading'] if heading == "": if len(obj_list) == 1: heading = 'Examples using ``{}``'.format(obj_list[0]) else: heading = 'Examples using one of multiple objects' lines.append(heading) heading_level = self.options.get('heading-level', '^') lines.append(heading_level * len(heading)) def has_backrefs(obj): src_dir = config.sphinx_gallery_conf['src_dir'] path = os.path.join(src_dir, backreferences_dir, '{}.examples'.format(obj)) return os.path.isfile(path) and os.path.getsize(path) > 0 if not any(has_backrefs(obj) for obj in obj_list): return [] # Insert the backreferences file(s) using the `include` directive. # This already includes the opening <div class="sphx-glr-thumbnails"> # and its closing </div>. for obj in obj_list: path = os.path.join('/', # Sphinx treats this as the source dir backreferences_dir, '{}.examples'.format(obj)) # Always remove the heading from the file lines.append("""\ .. include:: {} :start-after: start-sphx-glr-thumbnails""".format(path)) # Parse the assembly of `include` and `raw` directives text = '\n'.join(lines) include_lines = statemachine.string2lines(text, convert_whitespace=True) self.state_machine.insert_input(include_lines, path) return []
""" Image sg for responsive images """
[docs]class imgsgnode(nodes.General, nodes.Element): pass
[docs]def directive_boolean(value): if not value.strip(): raise ValueError("No argument provided but required") if value.lower().strip() in ["yes", "1", 1, "true", "ok"]: return True elif value.lower().strip() in ['no', '0', 0, 'false', 'none']: return False else: raise ValueError(u"Please use one of: yes, true, no, false. " u"Do not use `{}` as boolean.".format(value))
[docs]class ImageSg(images.Image): """ Implements a directive to allow an optional hidpi image. Meant to be used with the `image_srcset` configuration option. e.g.:: .. image-sg:: /plot_types/basic/images/sphx_glr_bar_001.png :alt: bar :srcset: /plot_types/basic/images/sphx_glr_bar_001.png, /plot_types/basic/images/sphx_glr_bar_001_2_00x.png 2.00x :class: sphx-glr-single-img The resulting html is:: <img src="sphx_glr_bar_001_hidpi.png" srcset="_images/sphx_glr_bar_001.png, _images/sphx_glr_bar_001_2_00x.png 2x", alt="bar" class="sphx-glr-single-img" /> """ has_content = False required_arguments = 1 optional_arguments = 3 final_argument_whitespace = False option_spec = { 'srcset': directives.unchanged, 'class': directives.class_option, 'alt': directives.unchanged, } def run(self): image_node = imgsgnode() imagenm = self.arguments[0] image_node['alt'] = self.options.get('alt', '') image_node['class'] = self.options.get('class', None) # we would like uri to be the highest dpi version so that # latex etc will use that. But for now, lets just make # imagenm image_node['uri'] = imagenm image_node['srcset'] = self.options.get('srcset', None) return [image_node]
def _parse_srcset(st): """ parse st""" entries = st.split(',') srcset = {} for entry in entries: spl = entry.strip().split(' ') if len(spl) == 1: srcset[0] = spl[0] elif len(spl) == 2: mult = spl[1][:-1] srcset[float(mult)] = spl[0] else: raise ExtensionError('srcset argument "{entry}" is invalid.') return srcset
[docs]def visit_imgsg_html(self, node): if node['srcset'] is None: self.visit_image(node) return imagedir, srcset = _copy_images(self, node) # /doc/examples/subd/plot_1.rst docsource = self.document['source'] # /doc/ # make sure to add the trailing slash: srctop = os.path.join(self.builder.srcdir, '') # examples/subd/plot_1.rst relsource = os.path.relpath(docsource, srctop) # /doc/build/html desttop = os.path.join(self.builder.outdir, '') # /doc/build/html/examples/subd dest = os.path.join(desttop, relsource) # ../../_images/ for dirhtml and ../_images/ for html imagerel = os.path.relpath(imagedir, os.path.dirname(dest)) if self.builder.name == "dirhtml": imagerel = os.path.join('..', imagerel, '') else: # html imagerel = os.path.join(imagerel, '') if '\\' in imagerel: imagerel = imagerel.replace('\\', '/') # make srcset str. Need to change all the prefixes! srcsetst = '' for mult in srcset: nm = os.path.basename(srcset[mult][1:]) # ../../_images/plot_1_2_0x.png relpath = imagerel+nm srcsetst += f'{relpath}' if mult == 0: srcsetst += ', ' else: srcsetst += f' {mult:1.2f}x, ' # trim trailing comma and space... srcsetst = srcsetst[:-2] # make uri also be relative... nm = os.path.basename(node['uri'][1:]) uri = imagerel + nm alt = node['alt'] if node['class'] is not None: classst = node['class'][0] classst = f'class = "{classst}"' else: classst = '' html_block = (f'<img src="{uri}" srcset="{srcsetst}" alt="{alt}"' + f' {classst}/>') self.body.append(html_block)
[docs]def visit_imgsg_latex(self, node): if node['srcset'] is not None: imagedir, srcset = _copy_images(self, node) maxmult = -1 # choose the highest res version for latex: for key in srcset.keys(): maxmult = max(maxmult, key) node['uri'] = str(PurePosixPath(srcset[maxmult]).name) self.visit_image(node)
def _copy_images(self, node): srcset = _parse_srcset(node['srcset']) # where the sources are. i.e. myproj/source srctop = self.builder.srcdir # copy image from source to imagedir. This is # *probably* supposed to be done by a builder but... # ie myproj/build/html/_images imagedir = os.path.join(self.builder.imagedir, '') imagedir = PurePosixPath(self.builder.outdir, imagedir) os.makedirs(imagedir, exist_ok=True) # copy all the sources to the imagedir: for mult in srcset: abspath = PurePosixPath(srctop, srcset[mult][1:]) shutil.copyfile(abspath, imagedir / abspath.name) return imagedir, srcset
[docs]def depart_imgsg_html(self, node): pass
[docs]def visit_sg_other(self, node): if node['uri'][0] == '/': node['uri'] = node['uri'][1:] self.visit_image(node)
[docs]def depart_imgsg_latex(self, node): self.depart_image(node)
[docs]def imagesg_addnode(app): app.add_node(imgsgnode, html=(visit_imgsg_html, depart_imgsg_html), latex=(visit_imgsg_latex, depart_imgsg_latex))