Coverage for pyquickhelper/sphinxext/sphinx_video_extension.py: 94%

160 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 sphinx extension to add button to share a page 

5""" 

6import os 

7import copy 

8import shutil 

9import sphinx 

10from docutils import nodes 

11from docutils.parsers.rst import Directive, directives 

12from sphinx.util.logging import getLogger 

13from sphinx.util import FilenameUniqDict 

14 

15 

16DEFAULT_CONFIG = dict( 

17 default_video_width='100%', 

18 default_video_height='auto', 

19 cache_path='_videos', 

20) 

21 

22 

23class video_node(nodes.General, nodes.Element): 

24 

25 """ 

26 Defines *video* node. 

27 """ 

28 pass 

29 

30 

31class VideoDirective(Directive): 

32 """ 

33 Adds video to a page. It can be done by adding:: 

34 

35 .. video:: filename.mp4 

36 :width: 400 

37 :height: 600 

38 

39 For latex, unit becomes *pt*. 

40 See `latex units <https://tex.stackexchange.com/questions/8260/what-are-the-various-units-ex-em-in-pt-bp-dd-pc-expressed-in-mm>`_. 

41 Videos are not enabled on latex by default, 

42 option ``:latex:`` must be set up. 

43 """ 

44 required_arguments = True 

45 optional_arguments = 0 

46 final_argument_whitespace = True 

47 option_spec = {'width': directives.unchanged, 

48 'height': directives.unchanged, 

49 'latex': directives.unchanged, 

50 } 

51 has_content = True 

52 video_class = video_node 

53 

54 def run(self): 

55 """ 

56 Runs the directive. 

57 

58 @return a list of nodes 

59 """ 

60 env = self.state.document.settings.env 

61 conf = env.app.config.videos_config 

62 docname = None if env is None else env.docname 

63 if docname is not None: 

64 docname = docname.replace("\\", "/").split("/")[-1] 

65 else: 

66 docname = '' # pragma: no cover 

67 

68 source = self.state.document.current_source 

69 filename = self.arguments[0] 

70 

71 if '://' in filename: 

72 logger = getLogger("video") 

73 logger.warning( 

74 "[video] url detected %r in docname %r - line %r.", 

75 filename, docname, self.lineno) 

76 is_url = True 

77 else: 

78 is_url = False 

79 

80 if not is_url: 

81 env.videos.add_file('', filename) 

82 

83 srcdir = env.srcdir 

84 rstrel = os.path.relpath(source, srcdir) 

85 rstfold = os.path.split(rstrel)[0] 

86 cache = os.path.join(srcdir, conf['cache_path']) 

87 vid = os.path.join(cache, filename) 

88 abspath = None 

89 relpath = None 

90 

91 if os.path.exists(vid): 

92 abspath = vid 

93 relpath = cache 

94 else: 

95 last = rstfold.replace('\\', '/') 

96 vid = os.path.join(srcdir, last, filename) 

97 if os.path.exists(vid): 

98 relpath = last 

99 abspath = vid 

100 

101 if abspath is None: 

102 logger = getLogger("video") 

103 logger.warning( 

104 "[video] Unable to find %r in docname %r - line %r - srcdir=%r.", 

105 filename, docname, self.lineno, srcdir) 

106 else: 

107 abspath = None 

108 relpath = None 

109 

110 width = self.options.get('width', conf['default_video_width']) 

111 height = self.options.get('height', conf['default_video_height']) 

112 latex = self.options.get('latex', False) in ( 

113 'True', 'true', True, 1, "1") 

114 

115 # build node 

116 node = self.__class__.video_class(uri=filename, docname=docname, lineno=self.lineno, 

117 width=width, height=height, abspath=abspath, 

118 relpath=relpath, is_url=is_url) 

119 node['classes'] += ["place-video"] 

120 node['video'] = filename 

121 node['latex'] = latex 

122 ns = [node] 

123 return ns 

124 

125 

126def visit_video_node(self, node): 

127 """ 

128 Visits a video node. 

129 Copies the video. 

130 """ 

131 if node['abspath'] is not None: 

132 outdir = self.builder.outdir 

133 relpath = os.path.join(outdir, node['relpath']) 

134 dname = os.path.split(node['uri'])[0] 

135 if dname: 

136 relpath = os.path.join(relpath, dname) 

137 if not os.path.exists(relpath): 

138 os.makedirs(relpath) 

139 shutil.copy(node['abspath'], relpath) 

140 logger = getLogger("video") 

141 logger.info("[video] copy %r to %r", node['uri'], relpath) 

142 

143 

144def _clean_value(val): 

145 if isinstance(val, tuple): 

146 return val[0] # pragma: no cover 

147 return val 

148 

149 

150def depart_video_node_html(self, node): 

151 """ 

152 What to do when leaving a node *video* 

153 the function should have different behaviour, 

154 depending on the format, or the setup should 

155 specify a different function for each. 

156 """ 

157 if node.hasattr("uri"): 

158 filename = node["uri"] 

159 width = _clean_value(node["width"]) 

160 height = _clean_value(node["height"]) 

161 found = node["abspath"] is not None or node["is_url"] 

162 if not found: 

163 body = f"<b>unable to find '{filename}'</b>" 

164 self.body.append(body) 

165 else: 

166 body = '<video{0}{1} controls><source src="{2}" type="video/{3}">Your browser does not support the video tag.</video>' 

167 width = f' width="{width}"' if width else "" 

168 height = f' height="{height}"' if height else "" 

169 body = body.format(width, height, filename, 

170 os.path.splitext(filename)[-1].strip('.')) 

171 self.body.append(body) 

172 

173 

174def depart_video_node_text(self, node): 

175 """ 

176 What to do when leaving a node *video* 

177 the function should have different behaviour, 

178 depending on the format, or the setup should 

179 specify a different function for each. 

180 """ 

181 if 'rst' in (self.builder.name, self.builder.format): 

182 depart_video_node_rst(self, node) 

183 elif 'latex' in (self.builder.name, self.builder.format): 

184 depart_video_node_latex(self, node) 

185 elif node.hasattr("uri"): 

186 filename = node["uri"] 

187 width = _clean_value(node["width"]) 

188 height = _clean_value(node["height"]) 

189 found = node["abspath"] is not None or node["is_url"] 

190 if not found: 

191 body = f"unable to find '{filename}'" 

192 self.body.append(body) 

193 else: 

194 body = '\nvideo {0}{1}: {2}\n' 

195 width = f' width="{width}"' if width else "" 

196 height = f' height="{height}"' if height else "" 

197 body = body.format(width, height, filename, 

198 os.path.splitext(filename)[-1].strip('.')) 

199 self.add_text(body) 

200 

201 

202def depart_video_node_latex(self, node): 

203 """ 

204 What to do when leaving a node *video* 

205 the function should have different behaviour, 

206 depending on the format, or the setup should 

207 specify a different function for each. 

208 """ 

209 if node.hasattr("uri"): 

210 width = _clean_value(node["width"]) 

211 height = _clean_value(node["height"]) 

212 full = os.path.join(node["relpath"], node['uri']) 

213 found = node['abspath'] is not None or node["is_url"] 

214 if not found: 

215 body = f"\\textbf{{unable to find '{full}'}}" 

216 self.body.append(body) 

217 else: 

218 def format_dim(s): 

219 "local function" 

220 if s == "auto" or s is None: 

221 return "{}" 

222 else: 

223 return f"{{{s}pt}}" 

224 body = '{3}\\includemovie[poster,autoplay,externalviewer,inline=false]{0}{1}{{{2}}}\n' 

225 width = format_dim(width) 

226 height = format_dim(height) 

227 full = full.replace('\\', '/').replace('_', '\\_') 

228 comment = '' if node['latex'] else '%' 

229 body = body.format(width, height, full, comment) 

230 self.body.append(body) 

231 

232 

233def depart_video_node_rst(self, node): 

234 """ 

235 What to do when leaving a node *video* 

236 the function should have different behaviour, 

237 depending on the format, or the setup should 

238 specify a different function for each. 

239 """ 

240 if node.hasattr("uri"): 

241 filename = node["uri"] 

242 width = _clean_value(node["width"]) 

243 height = _clean_value(node["height"]) 

244 found = node["abspath"] is not None or node["is_url"] 

245 if not found: 

246 body = f".. video:: {filename} [not found]" 

247 self.add_text(body + self.nl) 

248 else: 

249 body = f".. video:: {filename}" 

250 self.new_state(0) 

251 self.add_text(body + self.nl) 

252 if width: 

253 self.add_text(f' :width: {width}' + self.nl) 

254 if height: 

255 self.add_text(f' :height: {height}' + self.nl) 

256 self.end_state(wrap=False) 

257 

258 

259def initialize_videos_directive(app): 

260 """ 

261 Initializes the video directives. 

262 """ 

263 global DEFAULT_CONFIG 

264 

265 config = copy.deepcopy(DEFAULT_CONFIG) 

266 config.update(app.config.videos_config) 

267 app.config.videos_config = config 

268 app.env.videos = FilenameUniqDict() 

269 

270 

271def setup(app): 

272 """ 

273 setup for ``video`` (sphinx) 

274 """ 

275 global DEFAULT_CONFIG 

276 if hasattr(app, "add_mapping"): 

277 app.add_mapping('video', video_node) 

278 app.add_config_value('videos_config', DEFAULT_CONFIG, 'env') 

279 app.connect('builder-inited', initialize_videos_directive) 

280 app.add_node(video_node, 

281 html=(visit_video_node, depart_video_node_html), 

282 epub=(visit_video_node, depart_video_node_html), 

283 elatex=(visit_video_node, depart_video_node_latex), 

284 latex=(visit_video_node, depart_video_node_latex), 

285 rst=(visit_video_node, depart_video_node_rst), 

286 md=(visit_video_node, depart_video_node_rst), 

287 text=(visit_video_node, depart_video_node_text)) 

288 

289 app.add_directive('video', VideoDirective) 

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