Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 status_iterator, ensuredir, copyfile
13try:
14 from sphinx.util import relative_path
15except ImportError:
16 # Sphinx >= 3.0.0
17 from docutils.utils import relative_path
18from sphinx.util import logging
19from sphinx.locale import __
21try:
22 from sphinx.util import DownloadFiles
23except ImportError:
24 # Sphinx < 1.8
25 class DownloadFiles(dict):
26 def purge_doc(self, *args, **kwargs):
27 pass
29 def merge_other(self, *args, **kwargs):
30 pass
32 def add_file(self, docname, ref_filename):
33 self[docname] = (docname, ref_filename)
36class downloadlink_node(*addnodes.download_reference.__bases__):
38 """
39 Defines *download_reference* node.
40 """
41 pass
44def process_downloadlink_role(role, rawtext, text, lineno, inliner, options=None, content=None):
45 """
46 Defines custom role *downloadlink*. The following instructions defines
47 a link which can be displayed or hidden based on the output format.
48 The following directive creates a link to ``page.html`` only
49 for the HTML output, it also copies the files next to the source
50 and not in the folder ``_downloads``. The link does not push the user
51 to download the file but to see it.
53 ::
55 :downloadlink:`html::page.html`
57 :param role: The role name used in the document.
58 :param rawtext: The entire markup snippet, with role.
59 :param text: The text marked with the role.
60 :param lineno: The line number where rawtext appears in the input.
61 :param inliner: The inliner instance that called us.
62 :param options: Directive options for customization.
63 :param content: The directive content for customization.
65 The role only works for :epkg:`Sphinx` 1.8+.
66 """
67 if options is None:
68 options = {}
69 if content is None:
70 content = []
72 if '<' in text and '>' in text:
73 sep = text.split('<')
74 if len(sep) != 2:
75 msg = inliner.reporter.error(
76 "Unable to interpret '{0}' for downloadlink".format(text))
77 prb = inliner.problematic(rawtext, rawtext, msg)
78 return [prb], [msg]
79 name = sep[0].strip()
80 link = sep[1].strip('<>')
81 anchor = name
82 else:
83 name = text
84 link = text
85 anchor = os.path.split(text)[-1]
86 if '::' in anchor:
87 anchor = anchor.split('::')[-1].strip()
89 if '::' in link:
90 spl = link.split('::')
91 if len(spl) != 2:
92 msg = inliner.reporter.error(
93 "Unable to interpret '{0}' for downloadlink".format(text))
94 prb = inliner.problematic(rawtext, rawtext, msg)
95 return [prb], [msg]
96 out, src = spl
97 else:
98 ext = os.path.splitext(link)[-1]
99 out, src = ext.strip('.'), link
101 if "::" in src:
102 raise RuntimeError("Value '{0}' is unexpected.".format(src))
104 name = name.strip()
105 node = downloadlink_node(text=anchor, raw=text)
106 node['class'] = 'internal'
107 node['format'] = out
108 node['filename'] = src
109 node['reftarget'] = src
110 node['anchor'] = anchor
112 logger = logging.getLogger("downloadlink")
113 logger.info("[downloadlink] node '{0}'".format(str(node)))
115 return [node], []
118def visit_downloadlink_node_html(self, node):
119 """
120 Converts node *downloadlink* into :epkg:`html`.
121 """
122 if node['format'] != 'html':
123 raise nodes.SkipNode
125 logger = logging.getLogger("downloadlink")
126 logger.info("[downloadlink] HTML '{0}'".format(str(node)))
128 atts = {'class': 'reference'}
130 if not self.builder.download_support:
131 self.context.append('')
132 elif 'refuri' in node:
133 atts['class'] += ' external'
134 atts['href'] = node['refuri']
135 self.body.append(self.starttag(node, 'a', '', **atts))
136 self.context.append('</a>')
137 elif 'filename' in node:
138 atts['class'] += ' internal'
139 atts['href'] = node['filename']
140 self.body.append(self.starttag(node, 'a', '', **atts))
141 self.context.append('</a>')
142 else:
143 self.context.append('')
146def depart_downloadlink_node_html(self, node):
147 """
148 Converts node *downloadlink* into :epkg:`html`.
149 """
150 self.body.append(self.context.pop())
153def visit_downloadlink_node_latex(self, node):
154 """
155 Does notthing.
156 """
157 pass
160def depart_downloadlink_node_latex(self, node):
161 """
162 Does notthing.
163 """
164 pass
167def visit_downloadlink_node_text(self, node):
168 """
169 Does notthing.
170 """
171 if self.output_format in ('rst', 'md', "latex", "elatex"):
172 raise RuntimeError("format should not be '{0}' for base_class {1}".format(
173 self.output_format, self.base_class))
176def depart_downloadlink_node_text(self, node):
177 """
178 Does notthing.
179 """
180 if self.output_format in ('rst', 'md', "latex", "elatex"):
181 raise RuntimeError(
182 "format should not be '{0}'".format(self.output_format))
185def visit_downloadlink_node_rst(self, node):
186 """
187 Converts node *downloadlink* into :epkg:`rst`.
188 """
189 logger = logging.getLogger("downloadlink")
190 logger.info("[downloadlink] RST '{0}'".format(str(node)))
192 if node['format']:
193 self.add_text(":downloadlink:`{0} <{1}::{2}>`".format(
194 node["anchor"], node["format"], node["filename"]))
195 else:
196 self.add_text(":downloadlink:`{0} <{0}::{1}>`".format(
197 node["anchor"], node["filename"]))
198 raise nodes.SkipNode
201def depart_downloadlink_node_rst(self, node):
202 """
203 Converts node *downloadlink* into :epkg:`rst`.
204 """
205 pass
208def visit_downloadlink_node_md(self, node):
209 """
210 Converts node *downloadlink* into :epkg:`md`.
211 """
212 self.add_text("[{0}]({1})".format(node["anchor"], node["filename"]))
213 raise nodes.SkipNode
216def depart_downloadlink_node_md(self, node):
217 """
218 Converts node *downloadlink* into :epkg:`md`.
219 """
220 pass
223class DownloadLinkFileCollector(EnvironmentCollector):
224 """Download files collector for *sphinx.environment*."""
226 def check_attr(self, env):
227 if not hasattr(env, 'dllinkfiles'):
228 env.dllinkfiles = DownloadFiles()
230 def clear_doc(self, app, env, docname):
231 self.check_attr(env)
232 if env.dllinkfiles and len(env.dllinkfiles) > 0:
233 env.dllinkfiles.purge_doc(docname)
235 def merge_other(self, app, env, docnames, other):
236 logger = logging.getLogger("downloadlink")
237 logger.info("[downloadlink] merge")
238 self.check_attr(env)
239 env.dllinkfiles.merge_other(docnames, other.dllinkfiles)
241 def process_doc(self, app, doctree):
242 """Process downloadable file paths. """
243 self.check_attr(app.env)
244 nb = 0
245 for node in doctree.traverse(downloadlink_node):
246 format = node["format"]
247 if format and format != app.builder.format:
248 continue
249 nb += 1
250 dest = os.path.split(app.env.docname)[0]
251 name = node["filename"]
252 rel_filename = os.path.join(dest, name)
253 app.env.dependencies[app.env.docname].add(rel_filename)
254 node['dest'] = app.env.dllinkfiles.add_file(
255 app.env.docname, rel_filename)
256 if nb > 0:
257 logger = logging.getLogger("downloadlink")
258 logger.info("[downloadlink] processed {0}".format(nb))
261def copy_download_files(app, exc):
262 """
263 Copies all files mentioned with role *downloadlink*.
264 """
265 if exc:
266 builder = app.builder
267 logger = logging.getLogger("downloadlink")
268 mes = "Builder format '{0}'-'{1}', unable to copy file due to {2}".format(
269 builder.format, builder.__class__.__name__, exc)
270 logger.warning(mes)
271 return
273 def to_relpath(f):
274 return relative_path(app.srcdir, f)
275 # copy downloadable files
276 builder = app.builder
277 if builder.env.dllinkfiles:
278 logger = logging.getLogger("downloadlink")
279 logger.info("[downloadlink] copy_download_files")
280 for src in status_iterator(builder.env.dllinkfiles, __('copying downloadable(link) files... '),
281 "brown", len(
282 builder.env.dllinkfiles), builder.app.verbosity,
283 stringify_func=to_relpath):
284 docname, dest = builder.env.dllinkfiles[src]
285 relpath = set(os.path.dirname(dn) for dn in docname)
286 for rel in relpath:
287 dest = os.path.join(builder.outdir, rel)
288 ensuredir(os.path.dirname(dest))
289 shortname = os.path.split(src)[-1]
290 dest = os.path.join(dest, shortname)
291 name = os.path.join(builder.srcdir, src)
292 try:
293 copyfile(name, dest)
294 logger.info(
295 "[downloadlink] copy '{0}' to '{1}'".format(name, dest))
296 except FileNotFoundError:
297 mes = "Builder format '{0}'-'{3}', unable to copy file '{1}' into {2}'".format(
298 builder.format, name, dest, builder.__class__.__name__)
299 logger.warning(
300 "[downloadlink] cannot copy '{0}' to '{1}'".format(name, dest))
303def setup(app):
304 """
305 setup for ``bigger`` (sphinx)
306 """
307 app.add_env_collector(DownloadLinkFileCollector)
309 if hasattr(app, "add_mapping"):
310 app.add_mapping('downloadlink', downloadlink_node)
312 app.connect('build-finished', copy_download_files)
313 app.add_node(downloadlink_node,
314 html=(visit_downloadlink_node_html,
315 depart_downloadlink_node_html),
316 epub=(visit_downloadlink_node_html,
317 depart_downloadlink_node_html),
318 latex=(visit_downloadlink_node_latex,
319 depart_downloadlink_node_latex),
320 elatex=(visit_downloadlink_node_latex,
321 depart_downloadlink_node_latex),
322 text=(visit_downloadlink_node_text,
323 depart_downloadlink_node_text),
324 md=(visit_downloadlink_node_md,
325 depart_downloadlink_node_md),
326 rst=(visit_downloadlink_node_rst, depart_downloadlink_node_rst))
328 app.add_role('downloadlink', process_downloadlink_role)
329 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}