Coverage for pyquickhelper/sphinxext/sphinx_image_extension.py: 81%
185 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Defines a :epkg:`sphinx` extension to add button to share a page
5"""
6import os
7import copy
8import shutil
9from html import escape
10import sphinx
11from docutils import nodes
12from docutils.parsers.rst import Directive, directives
13from sphinx.util.logging import getLogger
14from sphinx.util import FilenameUniqDict
17DEFAULT_CONFIG = dict(
18 default_image_width=None,
19 default_image_height=None,
20 cache_path='_images',
21)
24class simpleimage_node(nodes.General, nodes.Element):
26 """
27 Defines *image* node.
28 """
29 pass
32class SimpleImageDirective(Directive):
33 """
34 Adds an image to a page. It can be done by adding::
36 .. simpleimage:: filename.png
37 :width: 400
38 :height: 600
40 Available options:
42 * ``:width:``, ``:height:``, ``:scale:``: resize the image
43 * ``:target:``: for HTML, clickable image
44 * ``:alt:``: for HTML
45 * ``:download:`` if the image is a url, it downloads the image.
46 * ``:convert:`` convert the image into a new format
47 """
48 required_arguments = True
49 optional_arguments = 0
50 final_argument_whitespace = True
51 option_spec = {'width': directives.unchanged,
52 'height': directives.unchanged,
53 'scale': directives.unchanged,
54 'target': directives.unchanged,
55 'alt': directives.unchanged,
56 'download': directives.unchanged,
57 'convert': directives.unchanged,
58 }
59 has_content = True
60 node_class = simpleimage_node
62 def run(self):
63 """
64 Runs the directive.
66 @return a list of nodes
67 """
68 env = self.state.document.settings.env
69 conf = env.app.config.simpleimages_config
70 docname = None if env is None else env.docname
71 if docname is not None:
72 docname = docname.replace("\\", "/").split("/")[-1]
73 else:
74 docname = ''
76 source = self.state.document.current_source
77 filename = self.arguments[0]
79 if '://' in filename:
80 logger = getLogger("simpleimage") # pragma: no cover
81 logger.warning( # pragma: no cover
82 "[simpleimage] url detected %r in docname %r - line %r"
83 ".", filename, docname, self.lineno)
84 is_url = True
85 else:
86 is_url = False
88 convert = self.options.get('convert', None)
89 if convert:
90 logger = getLogger("simpleimage") # pragma: no cover
91 logger.warning( # pragma: no cover
92 "[simpleimage] convert into %r not implemented for %r in "
93 "docname %r - line %r.",
94 convert, filename, docname, self.lineno)
96 download = self.options.get('download', None)
97 if convert:
98 logger = getLogger("simpleimage")
99 logger.warning( # pragma: no cover
100 "[simpleimage] download not implemented for %r in docname %r - line %r.",
101 filename, docname, self.lineno)
103 if not is_url:
104 env.images_mapping.add_file('', filename)
106 srcdir = env.srcdir
107 rstrel = os.path.relpath(source, srcdir)
108 rstfold = os.path.split(rstrel)[0]
109 cache = os.path.join(srcdir, conf['cache_path'])
110 img = os.path.join(cache, filename)
111 abspath = None
112 relpath = None
114 if os.path.exists(img):
115 abspath = img
116 relpath = cache
117 else:
118 last = rstfold.replace('\\', '/')
119 img = os.path.join(srcdir, last, filename)
120 if os.path.exists(img):
121 relpath = last
122 abspath = img
124 if abspath is None:
125 logger = getLogger("simpleimage") # pragma: no cover
126 logger.warning( # pragma: no cover
127 "[simpleimage] Unable to find %r in docname %r - line %r - srcdir=%r.",
128 filename, docname, self.lineno, srcdir)
129 else:
130 abspath = None
131 relpath = None
133 width = self.options.get('width', conf['default_image_width'])
134 height = self.options.get('height', conf['default_image_height'])
135 scale = self.options.get('scale', None)
136 alt = self.options.get('alt', None)
137 target = self.options.get('target', None)
139 # build node
140 node = self.__class__.node_class(uri=filename, docname=docname, lineno=self.lineno,
141 width=width, height=height, abspath=abspath,
142 relpath=relpath, is_url=is_url, alt=alt, scale=scale,
143 target=target, convert=convert, download=download)
144 node['classes'] += ["place-image"]
145 node['image'] = filename
146 ns = [node]
147 return ns
150def visit_simpleimage_node(self, node):
151 """
152 Visits a image node.
153 Copies the image.
154 """
155 if node['abspath'] is not None:
156 outdir = self.builder.outdir
157 relpath = os.path.join(outdir, node['relpath'])
158 dname = os.path.split(node['uri'])[0]
159 if dname:
160 relpath = os.path.join(relpath, dname)
161 if not os.path.exists(relpath):
162 os.makedirs(relpath)
163 if os.path.dirname(node['abspath']) != relpath:
164 shutil.copy(node['abspath'], relpath)
165 logger = getLogger("image") # pragma: no cover
166 logger.info("[image] copy '{0}' to '{1}'".format( # pragma: no cover
167 node['uri'], relpath))
170def _clean_value(val):
171 if isinstance(val, tuple):
172 return val[0]
173 return val
176def depart_simpleimage_node_html(self, node):
177 """
178 What to do when leaving a node *image*
179 the function should have different behaviour,
180 depending on the format, or the setup should
181 specify a different function for each.
182 """
183 if node.hasattr("uri"):
184 filename = node["uri"]
185 width = _clean_value(node["width"])
186 height = _clean_value(node["height"])
187 scale = node["scale"]
188 alt = node["alt"]
189 target = node["target"]
190 found = node["abspath"] is not None or node["is_url"]
191 if not found: # pragma: no cover
192 body = f"<b>unable to find '{filename}'</b>"
193 self.body.append(body)
194 else:
195 body = '<img src="{0}" {1} {2}/>'
196 width = f' width="{width}"' if width else ""
197 height = f' height="{height}"' if height else ""
198 if width or height:
199 style = f"{width}{height}"
200 elif scale:
201 style = f" width={scale}%"
202 alt = f' alt="{escape(alt)}"' if alt else ""
203 body = body.format(filename, style, alt)
204 if target:
205 body = f'<a href="{escape(target)}">{body}</a>'
206 self.body.append(body)
209def depart_simpleimage_node_text(self, node):
210 """
211 What to do when leaving a node *image*
212 the function should have different behaviour,
213 depending on the format, or the setup should
214 specify a different function for each.
215 """
216 if 'rst' in (self.builder.name, self.builder.format):
217 depart_simpleimage_node_rst(self, node)
218 elif 'md' in (self.builder.name, self.builder.format):
219 depart_simpleimage_node_md(self, node)
220 elif 'latex' in (self.builder.name, self.builder.format):
221 depart_simpleimage_node_latex(self, node)
222 elif node.hasattr("uri"):
223 filename = node["uri"]
224 width = _clean_value(node["width"])
225 height = _clean_value(node["height"])
226 scale = node["scale"]
227 alt = node["alt"]
228 target = node["target"]
229 found = node["abspath"] is not None or node["is_url"]
230 if not found: # pragma: no cover
231 body = f"unable to find '{filename}'"
232 self.body.append(body)
233 else:
234 body = '\nimage {0}{1}{2}: {3}{4}\n'
235 width = f' width="{width}"' if width else ""
236 height = f' height="{height}"' if height else ""
237 scale = f' scale="{scale}"' if scale else ""
238 alt = ' alt="{0}"'.format(alt.replace('"', '\\"')) if alt else ""
239 target = ' target="{0}"'.format(
240 target.replace('"', '\\"')) if target else ""
241 body = body.format(width, height, scale, filename, alt, target)
242 self.add_text(body)
245def depart_simpleimage_node_latex(self, node):
246 """
247 What to do when leaving a node *image*
248 the function should have different behaviour,
249 depending on the format, or the setup should
250 specify a different function for each.
251 """
252 if node.hasattr("uri"):
253 width = _clean_value(node["width"])
254 height = _clean_value(node["height"])
255 scale = node["scale"]
256 alt = node["alt"]
257 full = os.path.join(node["relpath"], node['uri'])
258 found = node['abspath'] is not None or node["is_url"]
259 if not found: # pragma: no cover
260 body = f"\\textbf{{unable to find '{full}'}}"
261 self.body.append(body)
262 else:
263 body = '\\includegraphics{0}{{{1}}}\n'
264 width = f"width={width}" if width else ""
265 height = f"height={height}" if height else ""
266 scale = f"scale={scale}" if scale else ""
267 if width or height or scale:
268 dims = [_ for _ in [width, height, scale] if _]
269 style = f"[{','.join(dims)}]"
270 else:
271 style = ""
272 alt = ' alt="{0}"'.format(alt.replace('"', '\\"')) if alt else ""
273 full = full.replace('\\', '/').replace('_', '\\_')
274 body = body.format(style, full)
275 self.body.append(body)
278def depart_simpleimage_node_rst(self, node):
279 """
280 What to do when leaving a node *image*
281 the function should have different behaviour,
282 depending on the format, or the setup should
283 specify a different function for each.
284 """
285 if node.hasattr("uri"):
286 filename = node["uri"]
287 found = node["abspath"] is not None or node["is_url"]
288 if not found: # pragma: no cover
289 body = f".. simpleimage:: {filename} [not found]"
290 self.add_text(body + self.nl)
291 else:
292 options = SimpleImageDirective.option_spec
293 body = f".. simpleimage:: {filename}"
294 self.new_state(0)
295 self.add_text(body + self.nl)
296 for opt in options:
297 v = node.get(opt, None)
298 if v:
299 self.add_text(f' :{opt}: {v}' + self.nl)
300 self.end_state(wrap=False)
303def depart_simpleimage_node_md(self, node):
304 """
305 What to do when leaving a node *image*
306 the function should have different behaviour,
307 depending on the format, or the setup should
308 specify a different function for each.
309 """
310 if node.hasattr("uri"):
311 filename = node["uri"]
312 found = node["abspath"] is not None or node["is_url"]
313 if not found: # pragma: no cover
314 body = f"[{filename}](not found)"
315 self.add_text(body + self.nl)
316 else:
317 alt = node.get("alt", "")
318 uri = filename
319 width = node.get('width', '').replace('px', '')
320 height = node.get('height', '').replace('px', '')
321 style = f" ={width}x{height}"
322 if style == " =x":
323 style = ""
324 text = f"![{alt}]({uri}{style})"
325 self.add_text(text)
328def initialize_simpleimages_directive(app):
329 """
330 Initializes the image directives.
331 """
332 global DEFAULT_CONFIG
334 config = copy.deepcopy(DEFAULT_CONFIG)
335 config.update(app.config.simpleimages_config)
336 app.config.simpleimages_config = config
337 app.env.images_mapping = FilenameUniqDict()
340def setup(app):
341 """
342 setup for ``image`` (sphinx)
343 """
344 global DEFAULT_CONFIG
345 if hasattr(app, "add_mapping"):
346 app.add_mapping('simpleimages_mapping', simpleimage_node)
347 app.add_config_value('simpleimages_config', DEFAULT_CONFIG, 'env')
348 app.connect('builder-inited', initialize_simpleimages_directive)
349 app.add_node(simpleimage_node,
350 html=(visit_simpleimage_node, depart_simpleimage_node_html),
351 epub=(visit_simpleimage_node, depart_simpleimage_node_html),
352 elatex=(visit_simpleimage_node,
353 depart_simpleimage_node_latex),
354 latex=(visit_simpleimage_node, depart_simpleimage_node_latex),
355 rst=(visit_simpleimage_node, depart_simpleimage_node_rst),
356 md=(visit_simpleimage_node, depart_simpleimage_node_md),
357 text=(visit_simpleimage_node, depart_simpleimage_node_text))
359 app.add_directive('simpleimage', SimpleImageDirective)
360 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}