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 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 '{0}' in docname '{1}' - line {2}.".format(filename, docname, self.lineno)) 

75 is_url = True 

76 else: 

77 is_url = False 

78 

79 if not is_url: 

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

81 

82 srcdir = env.srcdir 

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

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

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

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

87 abspath = None 

88 relpath = None 

89 

90 if os.path.exists(vid): 

91 abspath = vid 

92 relpath = cache 

93 else: 

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

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

96 if os.path.exists(vid): 

97 relpath = last 

98 abspath = vid 

99 

100 if abspath is None: 

101 logger = getLogger("video") 

102 logger.warning( 

103 "[video] Unable to find '{0}' in docname '{1}' - line {2} - srcdir='{3}'.".format(filename, docname, self.lineno, srcdir)) 

104 else: 

105 abspath = None 

106 relpath = None 

107 

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

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

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

111 'True', 'true', True, 1, "1") 

112 

113 # build node 

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

115 width=width, height=height, abspath=abspath, 

116 relpath=relpath, is_url=is_url) 

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

118 node['video'] = filename 

119 node['latex'] = latex 

120 ns = [node] 

121 return ns 

122 

123 

124def visit_video_node(self, node): 

125 """ 

126 Visits a video node. 

127 Copies the video. 

128 """ 

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

130 outdir = self.builder.outdir 

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

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

133 if dname: 

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

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

136 os.makedirs(relpath) 

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

138 logger = getLogger("video") 

139 logger.info("[video] copy '{0}' to '{1}'".format(node['uri'], relpath)) 

140 

141 

142def _clean_value(val): 

143 if isinstance(val, tuple): 

144 return val[0] # pragma: no cover 

145 return val 

146 

147 

148def depart_video_node_html(self, node): 

149 """ 

150 What to do when leaving a node *video* 

151 the function should have different behaviour, 

152 depending on the format, or the setup should 

153 specify a different function for each. 

154 """ 

155 if node.hasattr("uri"): 

156 filename = node["uri"] 

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

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

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

160 if not found: 

161 body = "<b>unable to find '{0}'</b>".format(filename) 

162 self.body.append(body) 

163 else: 

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

165 width = ' width="{0}"'.format(width) if width else "" 

166 height = ' height="{0}"'.format(height) if height else "" 

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

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

169 self.body.append(body) 

170 

171 

172def depart_video_node_text(self, node): 

173 """ 

174 What to do when leaving a node *video* 

175 the function should have different behaviour, 

176 depending on the format, or the setup should 

177 specify a different function for each. 

178 """ 

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

180 depart_video_node_rst(self, node) 

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

182 depart_video_node_latex(self, node) 

183 elif node.hasattr("uri"): 

184 filename = node["uri"] 

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

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

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

188 if not found: 

189 body = "unable to find '{0}'".format(filename) 

190 self.body.append(body) 

191 else: 

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

193 width = ' width="{0}"'.format(width) if width else "" 

194 height = ' height="{0}"'.format(height) if height else "" 

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

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

197 self.add_text(body) 

198 

199 

200def depart_video_node_latex(self, node): 

201 """ 

202 What to do when leaving a node *video* 

203 the function should have different behaviour, 

204 depending on the format, or the setup should 

205 specify a different function for each. 

206 """ 

207 if node.hasattr("uri"): 

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

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

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

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

212 if not found: 

213 body = "\\textbf{{unable to find '{0}'}}".format(full) 

214 self.body.append(body) 

215 else: 

216 def format_dim(s): 

217 "local function" 

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

219 return "{}" 

220 else: 

221 return "{{{0}pt}}".format(s) 

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

223 width = format_dim(width) 

224 height = format_dim(height) 

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

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

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

228 self.body.append(body) 

229 

230 

231def depart_video_node_rst(self, node): 

232 """ 

233 What to do when leaving a node *video* 

234 the function should have different behaviour, 

235 depending on the format, or the setup should 

236 specify a different function for each. 

237 """ 

238 if node.hasattr("uri"): 

239 filename = node["uri"] 

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

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

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

243 if not found: 

244 body = ".. video:: {0} [not found]".format(filename) 

245 self.add_text(body + self.nl) 

246 else: 

247 body = ".. video:: {0}".format(filename) 

248 self.new_state(0) 

249 self.add_text(body + self.nl) 

250 if width: 

251 self.add_text(' :width: {0}'.format(width) + self.nl) 

252 if height: 

253 self.add_text(' :height: {0}'.format(height) + self.nl) 

254 self.end_state(wrap=False) 

255 

256 

257def initialize_videos_directive(app): 

258 """ 

259 Initializes the video directives. 

260 """ 

261 global DEFAULT_CONFIG 

262 

263 config = copy.deepcopy(DEFAULT_CONFIG) 

264 config.update(app.config.videos_config) 

265 app.config.videos_config = config 

266 app.env.videos = FilenameUniqDict() 

267 

268 

269def setup(app): 

270 """ 

271 setup for ``video`` (sphinx) 

272 """ 

273 global DEFAULT_CONFIG 

274 if hasattr(app, "add_mapping"): 

275 app.add_mapping('video', video_node) 

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

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

278 app.add_node(video_node, 

279 html=(visit_video_node, depart_video_node_html), 

280 epub=(visit_video_node, depart_video_node_html), 

281 elatex=(visit_video_node, depart_video_node_latex), 

282 latex=(visit_video_node, depart_video_node_latex), 

283 rst=(visit_video_node, depart_video_node_rst), 

284 md=(visit_video_node, depart_video_node_rst), 

285 text=(visit_video_node, depart_video_node_text)) 

286 

287 app.add_directive('video', VideoDirective) 

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