Coverage for pyquickhelper/sphinxext/sphinx_downloadlink_extension.py: 65%
179 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 show a link instead of downloading it.
5This extension does not work for :epkg:`Sphinx` < 1.8.
6"""
7import os
8import sphinx
9from docutils import nodes
10from sphinx import addnodes
11from sphinx.environment.collectors import EnvironmentCollector
12from sphinx.util import ensuredir, copyfile
13try:
14 from sphinx.util.display import status_iterator
15except ImportError:
16 from sphinx.util import status_iterator
17try:
18 from sphinx.util import relative_path
19except ImportError:
20 # Sphinx >= 3.0.0
21 from docutils.utils import relative_path
22from sphinx.util import logging
23from sphinx.locale import __
25try:
26 from sphinx.util import DownloadFiles
27except ImportError:
28 # Sphinx < 1.8
29 class DownloadFiles(dict):
30 def purge_doc(self, *args, **kwargs):
31 pass
33 def merge_other(self, *args, **kwargs):
34 pass
36 def add_file(self, docname, ref_filename):
37 self[docname] = (docname, ref_filename)
40class downloadlink_node(*addnodes.download_reference.__bases__):
42 """
43 Defines *download_reference* node.
44 """
45 pass
48def process_downloadlink_role(role, rawtext, text, lineno, inliner, options=None, content=None):
49 """
50 Defines custom role *downloadlink*. The following instructions defines
51 a link which can be displayed or hidden based on the output format.
52 The following directive creates a link to ``page.html`` only
53 for the HTML output, it also copies the files next to the source
54 and not in the folder ``_downloads``. The link does not push the user
55 to download the file but to see it.
57 ::
59 :downloadlink:`html::page.html`
61 :param role: The role name used in the document.
62 :param rawtext: The entire markup snippet, with role.
63 :param text: The text marked with the role.
64 :param lineno: The line number where rawtext appears in the input.
65 :param inliner: The inliner instance that called us.
66 :param options: Directive options for customization.
67 :param content: The directive content for customization.
69 The role only works for :epkg:`Sphinx` 1.8+.
70 """
71 if options is None:
72 options = {}
73 if content is None:
74 content = []
76 if '<' in text and '>' in text:
77 sep = text.split('<')
78 if len(sep) != 2:
79 msg = inliner.reporter.error(
80 f"Unable to interpret '{text}' for downloadlink")
81 prb = inliner.problematic(rawtext, rawtext, msg)
82 return [prb], [msg]
83 name = sep[0].strip()
84 link = sep[1].strip('<>')
85 anchor = name
86 else:
87 name = text
88 link = text
89 anchor = os.path.split(text)[-1]
90 if '::' in anchor:
91 anchor = anchor.split('::')[-1].strip()
93 if '::' in link:
94 spl = link.split('::')
95 if len(spl) != 2:
96 msg = inliner.reporter.error(
97 f"Unable to interpret '{text}' for downloadlink")
98 prb = inliner.problematic(rawtext, rawtext, msg)
99 return [prb], [msg]
100 out, src = spl
101 else:
102 ext = os.path.splitext(link)[-1]
103 out, src = ext.strip('.'), link
105 if "::" in src:
106 raise RuntimeError(f"Value '{src}' is unexpected.")
108 name = name.strip()
109 node = downloadlink_node(text=anchor, raw=text)
110 node['class'] = 'internal'
111 node['format'] = out
112 node['filename'] = src
113 node['reftarget'] = src
114 node['anchor'] = anchor
116 logger = logging.getLogger("downloadlink")
117 logger.info("[downloadlink] node %s", node)
119 return [node], []
122def visit_downloadlink_node_html(self, node):
123 """
124 Converts node *downloadlink* into :epkg:`html`.
125 """
126 if node['format'] != 'html':
127 raise nodes.SkipNode
129 logger = logging.getLogger("downloadlink")
130 logger.info("[downloadlink] HTML %s", node)
132 atts = {'class': 'reference'}
134 if not self.builder.download_support:
135 self.context.append('')
136 elif 'refuri' in node:
137 atts['class'] += ' external'
138 atts['href'] = node['refuri']
139 self.body.append(self.starttag(node, 'a', '', **atts))
140 self.context.append('</a>')
141 elif 'filename' in node:
142 atts['class'] += ' internal'
143 atts['href'] = node['filename']
144 self.body.append(self.starttag(node, 'a', '', **atts))
145 self.context.append('</a>')
146 else:
147 self.context.append('')
150def depart_downloadlink_node_html(self, node):
151 """
152 Converts node *downloadlink* into :epkg:`html`.
153 """
154 self.body.append(self.context.pop())
157def visit_downloadlink_node_latex(self, node):
158 """
159 Does notthing.
160 """
161 pass
164def depart_downloadlink_node_latex(self, node):
165 """
166 Does notthing.
167 """
168 pass
171def visit_downloadlink_node_text(self, node):
172 """
173 Does notthing.
174 """
175 if self.output_format in ('rst', 'md', "latex", "elatex"):
176 raise RuntimeError("format should not be '{0}' for base_class {1}".format(
177 self.output_format, self.base_class))
180def depart_downloadlink_node_text(self, node):
181 """
182 Does notthing.
183 """
184 if self.output_format in ('rst', 'md', "latex", "elatex"):
185 raise RuntimeError(
186 f"format should not be '{self.output_format}'")
189def visit_downloadlink_node_rst(self, node):
190 """
191 Converts node *downloadlink* into :epkg:`rst`.
192 """
193 logger = logging.getLogger("downloadlink")
194 logger.info("[downloadlink] RST %s", node)
196 if node['format']:
197 self.add_text(":downloadlink:`{0} <{1}::{2}>`".format(
198 node["anchor"], node["format"], node["filename"]))
199 else:
200 self.add_text(":downloadlink:`{0} <{0}::{1}>`".format(
201 node["anchor"], node["filename"]))
202 raise nodes.SkipNode
205def depart_downloadlink_node_rst(self, node):
206 """
207 Converts node *downloadlink* into :epkg:`rst`.
208 """
209 pass
212def visit_downloadlink_node_md(self, node):
213 """
214 Converts node *downloadlink* into :epkg:`md`.
215 """
216 self.add_text(f"[{node['anchor']}]({node['filename']})")
217 raise nodes.SkipNode
220def depart_downloadlink_node_md(self, node):
221 """
222 Converts node *downloadlink* into :epkg:`md`.
223 """
224 pass
227class DownloadLinkFileCollector(EnvironmentCollector):
228 """Download files collector for *sphinx.environment*."""
230 def check_attr(self, env):
231 if not hasattr(env, 'dllinkfiles'):
232 env.dllinkfiles = DownloadFiles()
234 def clear_doc(self, app, env, docname):
235 self.check_attr(env)
236 if env.dllinkfiles and len(env.dllinkfiles) > 0:
237 env.dllinkfiles.purge_doc(docname)
239 def merge_other(self, app, env, docnames, other):
240 logger = logging.getLogger("downloadlink")
241 logger.info("[downloadlink] merge")
242 self.check_attr(env)
243 env.dllinkfiles.merge_other(docnames, other.dllinkfiles)
245 def process_doc(self, app, doctree):
246 """Process downloadable file paths. """
247 self.check_attr(app.env)
248 nb = 0
249 for node in doctree.traverse(downloadlink_node):
250 format = node["format"]
251 if format and format != app.builder.format:
252 continue
253 nb += 1
254 dest = os.path.split(app.env.docname)[0]
255 name = node["filename"]
256 rel_filename = os.path.join(dest, name)
257 app.env.dependencies[app.env.docname].add(rel_filename)
258 node['dest'] = app.env.dllinkfiles.add_file(
259 app.env.docname, rel_filename)
260 if nb > 0:
261 logger = logging.getLogger("downloadlink")
262 logger.info("[downloadlink] processed %r", nb)
265def copy_download_files(app, exc):
266 """
267 Copies all files mentioned with role *downloadlink*.
268 """
269 if exc:
270 builder = app.builder
271 logger = logging.getLogger("downloadlink")
272 mes = "Builder format '{0}'-'{1}', unable to copy file due to {2}".format(
273 builder.format, builder.__class__.__name__, exc)
274 logger.warning(mes)
275 return
277 def to_relpath(f):
278 return relative_path(app.srcdir, f)
279 # copy downloadable files
280 builder = app.builder
281 if builder.env.dllinkfiles:
282 logger = logging.getLogger("downloadlink")
283 logger.info("[downloadlink] copy_download_files")
284 for src in status_iterator(builder.env.dllinkfiles, __('copying downloadable(link) files... '),
285 "brown", len(
286 builder.env.dllinkfiles), builder.app.verbosity,
287 stringify_func=to_relpath):
288 docname, dest = builder.env.dllinkfiles[src]
289 relpath = set(os.path.dirname(dn) for dn in docname)
290 for rel in relpath:
291 dest = os.path.join(builder.outdir, rel)
292 ensuredir(os.path.dirname(dest))
293 shortname = os.path.split(src)[-1]
294 dest = os.path.join(dest, shortname)
295 name = os.path.join(builder.srcdir, src)
296 try:
297 copyfile(name, dest)
298 logger.info("[downloadlink] copy %r to %r", name, dest)
299 except FileNotFoundError:
300 mes = "Builder format '{0}'-'{3}', unable to copy file '{1}' into {2}'".format(
301 builder.format, name, dest, builder.__class__.__name__)
302 logger.warning(
303 "[downloadlink] cannot copy %r to %r", name, dest)
306def setup(app):
307 """
308 setup for ``bigger`` (sphinx)
309 """
310 app.add_env_collector(DownloadLinkFileCollector)
312 if hasattr(app, "add_mapping"):
313 app.add_mapping('downloadlink', downloadlink_node)
315 app.connect('build-finished', copy_download_files)
316 app.add_node(downloadlink_node,
317 html=(visit_downloadlink_node_html,
318 depart_downloadlink_node_html),
319 epub=(visit_downloadlink_node_html,
320 depart_downloadlink_node_html),
321 latex=(visit_downloadlink_node_latex,
322 depart_downloadlink_node_latex),
323 elatex=(visit_downloadlink_node_latex,
324 depart_downloadlink_node_latex),
325 text=(visit_downloadlink_node_text,
326 depart_downloadlink_node_text),
327 md=(visit_downloadlink_node_md,
328 depart_downloadlink_node_md),
329 rst=(visit_downloadlink_node_rst, depart_downloadlink_node_rst))
331 app.add_role('downloadlink', process_downloadlink_role)
332 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}