Source code for setupdocx.sphinx.ext.imagewrap
# -*- coding: utf-8 -*-
"""Wrapper extension for the directive *image* / *Image* and *figure/Figure*.
In order to avoid copy-of-code or mix-in, the instance is processed by functions.
"""
import os
import re
import copy
from docutils.parsers.rst import directives
from docutils.parsers.rst.directives import images
import setupdocx
__author__ = 'Arno-Can Uestuensoez'
__author_email__ = 'acue_sf2@sourceforge.net'
__license__ = "Artistic-License-2.0 + Forced-Fairplay-Constraints"
__copyright__ = "Copyright (C) 2019 Arno-Can Uestuensoez @Ingenieurbuero Arno-Can Uestuensoez"
__uuid__ = "45167c30-3261-4a38-9de4-d7151348ba48"
URI_PATTERN = re.compile(r'^\w+://')
#: helper for path normalization
[docs]class ImageWrapError(setupdocx.SetupDocXError): # pylint: disable=too-few-public-methods
"""Image wrapper failed."""
pass
[docs]class FigureWrapError(ImageWrapError): # pylint: disable=too-few-public-methods
"""Figure wrapper failed."""
pass
_UP = '..' + os.sep
[docs]def align_paths_to_top(path, base, rel, depth):
"""Aligns the provided image/figure relative paths. Checks first
the existence of the path, if not exists adds the upward relative
path as offset and checks the existence again. In case of a match
the image path is replace by the existent, else kept unchanged.
Ignores absolute paths.
Args:
path:
Path to be aligned. Only non-absolute paths are processed.
base:
Base for relative input. Either absolute or relative to
current position.
rel:
Relative paths as side-branch of the search.
depth:
Maximum number of upward directory nodes to be searched.
Returns:
Resolved path, either absolute or relative.
Raises:
"""
if (
not URI_PATTERN.match(path)
and path and not os.path.isabs(path)
and not os.path.exists(base + rel + path)
and os.path.exists(base + path)
):
return _UP * depth + path
return path
[docs]def cb_align_values_image(argument):
"""Imported callback, see:
* docutils.parsers.rst.directives
* docutils.parsers.rst.directives.images.Image.align_values
"""
return directives.choice(argument, images.Image.align_values)
[docs]class ImageExt(object):
"""Wraps the class *Image*. Passes builder specific values.
For the image parameters refer to [restdir]_.
.. note::
When multiple builder are called, requires to run with
the cache erase option. ::
sphinx-build -E ..,
The wrapper is designed to be implemented as a mixin only.
"""
#: provided additional options
option_spec_image = {
'scale-html': directives.percentage,
'scale-singlehtml': directives.percentage,
'scale-latex': directives.percentage,
'scale-epub': directives.percentage,
'scale-epub2': directives.percentage,
'scale-mobi': directives.percentage,
'height-html': directives.length_or_percentage_or_unitless,
'height-singlehtml': directives.length_or_percentage_or_unitless,
'height-latex': directives.length_or_percentage_or_unitless,
'height-epub': directives.length_or_percentage_or_unitless,
'height-epub2': directives.length_or_percentage_or_unitless,
'height-mobi': directives.length_or_percentage_or_unitless,
'width-html': directives.length_or_percentage_or_unitless,
'width-singlehtml': directives.length_or_percentage_or_unitless,
'width-latex': directives.length_or_percentage_or_unitless,
'width-epub': directives.length_or_percentage_or_unitless,
'width-epub2': directives.length_or_percentage_or_unitless,
'width-mobi': directives.length_or_percentage_or_unitless,
'target-html': directives.uri,
'target-singlehtml': directives.uri,
'target-latex': directives.uri,
'target-epub': directives.uri,
'target-epub2': directives.uri,
'target-mobi': directives.uri,
'align': cb_align_values_image,
} #: wrap all image parameters
#: complete options
option_spec = copy.copy(option_spec_image)
def __init__(self, *args, **kargs):
super(ImageExt, self).__init__(*args, **kargs)
self._env = None
self._builder = None
self.own_opts = {}
[docs] def align_paths_to_top(self, res):
"""Aligns the provided image/figure relative paths. Checks first
the existence of the path, if not exists adds the upward relative
path as offset and checks the existence again. In case of a match
the image path is replace by the existent, else kept unchanged.
Ignores items with absolute paths.
Aligns::
source-path
target-html
target
REMARK: Had to reengineer a bit of redundant attributes - so eventually some
additional fixes may be required in future.
Args:
res:
result from call of 'images.Image.run()'.
Returns:
None, updates 'res'.
Raises:
pass-through
"""
env = self.state.document.settings.env #pylint: disable=E1101
_base = env.srcdir + os.sep
_rel = os.path.dirname(env.docname) + os.sep
_depth = len(_rel.split(os.sep)) - 1
_opts = self.own_opts
# normalize the source link - for target links
res[0]['uri'] = align_paths_to_top(self.arguments[0], _base, _rel, _depth) #pylint: disable=E1101
#
#TODO: final validation required for portability
#
# normalize additional attributes - suppress a bunch of fake-warnings
#
# satisfies the ImageCollector in the final step, which uses the uri of the image child.
self.arguments[0] = res[0]['uri'] #pylint: disable=E1101
try:
res[0].children[0]['targethtml'] = res[0]['uri']
except:
pass
try:
res[0].children[0]['uri'] = res[0]['uri']
except:
pass
# seems to be analsed once before reaching this point: res[0].children[0]rawsource = ...
# normalize the reference target
# want the priority hierarchy, thus prefetch all known
_f0 = None
for k in _opts:
# should be one only for currenty builder
if k.startswith('target-') and k in self.option_spec_image.keys():
_f0 = _opts.get(k)
# an optional generic - e.g. as default for under-construction
_f1 = _opts.get('target')
if _f0 and not os.path.isabs(_f0):
res[0]['refuri'] = align_paths_to_top(_f0, _base, _rel, _depth)
elif _f1 and not os.path.isabs(_f1):
res[0]['refuri'] = align_paths_to_top(_f1, _base, _rel, _depth)
# do not need it, but keep it as reminder
#else:
# res[0]['refuri'] = align_paths_to_top(self.arguments[0], _base, _rel, _depth)
[docs] def extract_own_options(self):
"""Process the parameters, and maps the values to the parent class *Image*.
The values are provided by the member *self.options*. ::
scale-{html, singlehtml, latex, epub, epub2, mobi,}
height-{html, singlehtml, latex, epub, epub2, mobi,}
width-{html, singlehtml, latex, epub, epub2, mobi,}
align-{html, singlehtml, latex, epub, epub2, mobi,}
target-{html, singlehtml, latex, epub, epub2, mobi,}
Defaults values are::
scale
height
width
align
target
Fetches the known-and-present special options only from 'self.options'.
These are stored in 'self.own_opts', the original default values remain
untouched.
"""
builder = self._builder
opts = self.options #pylint: disable=E1101
_opts = self.own_opts
if opts is None:
return opts
# set the specifics for the builder, or keep the generic
try:
_opts['width-' + builder] = opts.pop('width-' + builder)
except KeyError:
pass
try:
_opts['height-' + builder] = opts.pop('height-' + builder)
except KeyError:
pass
try:
_opts['scale-' + builder] = opts.pop('scale-' + builder)
except KeyError:
pass
try:
_opts['target-' + builder] = opts.pop('target-' + builder)
except KeyError:
pass
# now remove unused special options
_for_del = []
for k in opts.keys():
_k = re.sub(r'.*-([^-]+)$', r'\1', k)
if _k in self.option_spec_image.keys():
_for_del.append(k)
for k in _for_del:
opts.pop(k)
return _opts
[docs] def set_own_options(self, res):
"""Updates the options of the result with the own extention-options.
::
scale-{html, latex, epub, mobi,} => scale
height-{html, latex, epub, mobi,} => height
width-{html, latex, epub, mobi,} => width
align-{html, latex, epub, mobi,} => align
target-{html, latex, epub, mobi,} => target
Args:
res:
result from call of 'images.Image.run()'.
Returns:
None, updates 'res'.
Raises:
pass-through
"""
if self.own_opts is None:
# nothing to do
return
# align contained source paths such as uri and target
self.align_paths_to_top(res)
# non-path option mappings
_width = self.own_opts.get('width-' + self._builder)
_height = self.own_opts.get('height-' + self._builder)
_scale = self.own_opts.get('scale-' + self._builder)
if _width:
res[0]['width'] = _width
if _height:
res[0]['height'] = _height
if _scale:
res[0]['scale'] = _scale
[docs] def run(self):
"""Process the parameters by *self.setoptions*, and calls
the run method of the parent class *Image*. Inherits
the option_spec from *docutils.parsers.rst.directives.images.Image*.
Adds the following parameters. ::
scale-{html, latex, epub, mobi,}
height-{html, latex, epub, mobi,}
width-{html, latex, epub, mobi,}
align-{html, latex, epub, mobi,}
target-{html, latex, epub, mobi,}
"""
# fetch builder
self._env = self.state.document.settings.env #pylint: disable=E1101
self._builder = self._env.app.builder.name
# initialize the wrapper extensions
# pop-out additional options
self.extract_own_options()
# process original base class
res = super(ImageExt, self).run() #pylint: disable=E1101
# sets the extension options
self.set_own_options(res)
return res
[docs]class FigureExt(ImageExt):
"""Wraps the class *Figure*. Passes builder specific values.
For the figure parameters refer to [restdir]_.
.. note::
When multiple builder are called, requires to run with
the cache erase option. ::
sphinx-build -E ..,
The permitted values of some parameters are slightly different from
the parent image classes [restdir]_. This is taken into account.
For the complete parameter description refer to [restdir]_, here a copy
of the different semantics of "*width*" and "*figwidth*".
This option [figwidth] does not scale the included image;
use the "width" image option for that. ::
+---------------------------+
| figure |
| |
|<------ figwidth --------->|
| |
| +---------------------+ |
| | image | |
| | | |
| |<--- width --------->| |
| +---------------------+ |
| |
|The figure's caption should|
|wrap at this width. |
+---------------------------+
The wrapper is designed to be implemented as a mixin only.
"""
option_spec_figure = {
'figwidth-html': directives.length_or_percentage_or_unitless,
'figwidth-singlehtml': directives.length_or_percentage_or_unitless,
'figwidth-latex': directives.length_or_percentage_or_unitless,
'figwidth-epub': directives.length_or_percentage_or_unitless,
'figwidth-epub2': directives.length_or_percentage_or_unitless,
'figwidth-mobi': directives.length_or_percentage_or_unitless,
'figclass-html': directives.length_or_unitless,
'figclass-singlehtml': directives.length_or_unitless,
'figclass-latex': directives.length_or_unitless,
'figclass-epub': directives.length_or_unitless,
'figclass-epub2': directives.length_or_unitless,
'figclass-mobi': directives.length_or_unitless,
} #: wrap all image parameters
option_spec = copy.copy(option_spec_figure)
option_spec.update(ImageExt.option_spec_image)
def __init__(self, *args, **kargs):
super(FigureExt, self).__init__(*args, **kargs)
self.own_opts = {}
[docs] def extract_own_options(self):
"""Process the parameters, and maps the values to the parent class *Figure*.
The values are provided by the member *self.options*. ::
figwidth-{html, latex, epub, mobi,}
figclass-{html, latex, epub, mobi,}
Fetches the known-and-present special options only from 'self.options'.
These are stored in 'self.own_opts', the original default values remain
untouched.
"""
env = self.state.document.settings.env #pylint: disable=E1101
builder = env.app.builder.name
opts = self.options #pylint: disable=E1101
_opts = self.own_opts
if opts is None:
return _opts
# set the specifics for the builder, or keep the generic
try:
_opts['figwidth'] = opts.pop('figwidth-' + builder)
except KeyError:
pass
return _opts
[docs] def align_figure(self, argument): #pylint: disable=R0201
return directives.choice(argument, images.Figure.align_h_values)
[docs] def setoptions_figure(self, inst): #pylint: disable=R0201
"""Process the parameters, and maps the values to the parent class *Figure*.
The values are provided by the member *self.options*. ::
figwidth-{html, latex, epub, mobi,}
figclass-{html, latex, epub, mobi,}
"""
env = inst.state.document.settings.env
builder = env.app.builder.name
opts = inst.options
if opts is None:
return opts
_x = opts.get('figwidth-' + builder, None)
if _x != None:
opts['figwidth'] = _x
_x = opts.get('figclass-' + builder, None)
if _x != None:
opts['figclass'] = _x
return opts
[docs] def run(self):
"""Process the parameters by *self.setoptions*, and calls
the run method of the parent class *Figure*. Inherits
the option_spec from *docutils.parsers.rst.directives.images.Figure*.
Adds the following parameters. ::
figwidth-{html, latex, epub, mobi,}
figclass-{html, latex, epub, mobi,}
"""
# initialize the wrapper extensions
# pop-out additional options
self.extract_own_options()
# process original base class
res = super(FigureExt, self).run()
# align contained source paths
self.align_paths_to_top(res)
return res
[docs]def setup(app):
"""Initialize the extension."""
#
# extended image wrapper
#
class ImageWrap(ImageExt, images.Image): #pylint: disable=C0111
pass
for name, value in images.Image.option_spec.items():
ImageWrap.option_spec[name] = value
for name, value in ImageExt.option_spec.items():
ImageWrap.option_spec[name] = value
app.add_directive('imagewrap', ImageWrap)
#
# extended figure wrapper
#
class FigureWrap(FigureExt, images.Figure): #pylint: disable=C0111
pass
for name, value in images.Image.option_spec.items():
FigureWrap.option_spec[name] = value
for name, value in FigureExt.option_spec.items():
FigureWrap.option_spec[name] = value
app.add_directive('figurewrap', FigureWrap)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}