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
16DEFAULT_CONFIG = dict(
17 default_video_width='100%',
18 default_video_height='auto',
19 cache_path='_videos',
20)
23class video_node(nodes.General, nodes.Element):
25 """
26 Defines *video* node.
27 """
28 pass
31class VideoDirective(Directive):
32 """
33 Adds video to a page. It can be done by adding::
35 .. video:: filename.mp4
36 :width: 400
37 :height: 600
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
54 def run(self):
55 """
56 Runs the directive.
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
68 source = self.state.document.current_source
69 filename = self.arguments[0]
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
79 if not is_url:
80 env.videos.add_file('', filename)
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
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
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
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")
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
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))
142def _clean_value(val):
143 if isinstance(val, tuple):
144 return val[0] # pragma: no cover
145 return val
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)
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)
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)
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)
257def initialize_videos_directive(app):
258 """
259 Initializes the video directives.
260 """
261 global DEFAULT_CONFIG
263 config = copy.deepcopy(DEFAULT_CONFIG)
264 config.update(app.config.videos_config)
265 app.config.videos_config = config
266 app.env.videos = FilenameUniqDict()
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))
287 app.add_directive('video', VideoDirective)
288 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}