Hide keyboard shortcuts

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 __ 

20 

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 

28 

29 def merge_other(self, *args, **kwargs): 

30 pass 

31 

32 def add_file(self, docname, ref_filename): 

33 self[docname] = (docname, ref_filename) 

34 

35 

36class downloadlink_node(*addnodes.download_reference.__bases__): 

37 

38 """ 

39 Defines *download_reference* node. 

40 """ 

41 pass 

42 

43 

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. 

52 

53 :: 

54 

55 :downloadlink:`html::page.html` 

56 

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. 

64 

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 = [] 

71 

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() 

88 

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 

100 

101 if "::" in src: 

102 raise RuntimeError("Value '{0}' is unexpected.".format(src)) 

103 

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 

111 

112 logger = logging.getLogger("downloadlink") 

113 logger.info("[downloadlink] node '{0}'".format(str(node))) 

114 

115 return [node], [] 

116 

117 

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 

124 

125 logger = logging.getLogger("downloadlink") 

126 logger.info("[downloadlink] HTML '{0}'".format(str(node))) 

127 

128 atts = {'class': 'reference'} 

129 

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('') 

144 

145 

146def depart_downloadlink_node_html(self, node): 

147 """ 

148 Converts node *downloadlink* into :epkg:`html`. 

149 """ 

150 self.body.append(self.context.pop()) 

151 

152 

153def visit_downloadlink_node_latex(self, node): 

154 """ 

155 Does notthing. 

156 """ 

157 pass 

158 

159 

160def depart_downloadlink_node_latex(self, node): 

161 """ 

162 Does notthing. 

163 """ 

164 pass 

165 

166 

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)) 

174 

175 

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)) 

183 

184 

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))) 

191 

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 

199 

200 

201def depart_downloadlink_node_rst(self, node): 

202 """ 

203 Converts node *downloadlink* into :epkg:`rst`. 

204 """ 

205 pass 

206 

207 

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 

214 

215 

216def depart_downloadlink_node_md(self, node): 

217 """ 

218 Converts node *downloadlink* into :epkg:`md`. 

219 """ 

220 pass 

221 

222 

223class DownloadLinkFileCollector(EnvironmentCollector): 

224 """Download files collector for *sphinx.environment*.""" 

225 

226 def check_attr(self, env): 

227 if not hasattr(env, 'dllinkfiles'): 

228 env.dllinkfiles = DownloadFiles() 

229 

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) 

234 

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) 

240 

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)) 

259 

260 

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 

272 

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)) 

301 

302 

303def setup(app): 

304 """ 

305 setup for ``bigger`` (sphinx) 

306 """ 

307 app.add_env_collector(DownloadLinkFileCollector) 

308 

309 if hasattr(app, "add_mapping"): 

310 app.add_mapping('downloadlink', downloadlink_node) 

311 

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)) 

327 

328 app.add_role('downloadlink', process_downloadlink_role) 

329 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}