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

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 __ 

24 

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 

32 

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

34 pass 

35 

36 def add_file(self, docname, ref_filename): 

37 self[docname] = (docname, ref_filename) 

38 

39 

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

41 

42 """ 

43 Defines *download_reference* node. 

44 """ 

45 pass 

46 

47 

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. 

56 

57 :: 

58 

59 :downloadlink:`html::page.html` 

60 

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. 

68 

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

75 

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

92 

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 

104 

105 if "::" in src: 

106 raise RuntimeError(f"Value '{src}' is unexpected.") 

107 

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 

115 

116 logger = logging.getLogger("downloadlink") 

117 logger.info("[downloadlink] node %s", node) 

118 

119 return [node], [] 

120 

121 

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 

128 

129 logger = logging.getLogger("downloadlink") 

130 logger.info("[downloadlink] HTML %s", node) 

131 

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

133 

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

148 

149 

150def depart_downloadlink_node_html(self, node): 

151 """ 

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

153 """ 

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

155 

156 

157def visit_downloadlink_node_latex(self, node): 

158 """ 

159 Does notthing. 

160 """ 

161 pass 

162 

163 

164def depart_downloadlink_node_latex(self, node): 

165 """ 

166 Does notthing. 

167 """ 

168 pass 

169 

170 

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

178 

179 

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}'") 

187 

188 

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) 

195 

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 

203 

204 

205def depart_downloadlink_node_rst(self, node): 

206 """ 

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

208 """ 

209 pass 

210 

211 

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 

218 

219 

220def depart_downloadlink_node_md(self, node): 

221 """ 

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

223 """ 

224 pass 

225 

226 

227class DownloadLinkFileCollector(EnvironmentCollector): 

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

229 

230 def check_attr(self, env): 

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

232 env.dllinkfiles = DownloadFiles() 

233 

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) 

238 

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) 

244 

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) 

263 

264 

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 

276 

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) 

304 

305 

306def setup(app): 

307 """ 

308 setup for ``bigger`` (sphinx) 

309 """ 

310 app.add_env_collector(DownloadLinkFileCollector) 

311 

312 if hasattr(app, "add_mapping"): 

313 app.add_mapping('downloadlink', downloadlink_node) 

314 

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

330 

331 app.add_role('downloadlink', process_downloadlink_role) 

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