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
« 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
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 %r in docname %r - line %r.",
75 filename, docname, self.lineno)
76 is_url = True
77 else:
78 is_url = False
80 if not is_url:
81 env.videos.add_file('', filename)
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
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
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
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")
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
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)
144def _clean_value(val):
145 if isinstance(val, tuple):
146 return val[0] # pragma: no cover
147 return val
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)
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)
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)
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)
259def initialize_videos_directive(app):
260 """
261 Initializes the video directives.
262 """
263 global DEFAULT_CONFIG
265 config = copy.deepcopy(DEFAULT_CONFIG)
266 config.update(app.config.videos_config)
267 app.config.videos_config = config
268 app.env.videos = FilenameUniqDict()
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))
289 app.add_directive('video', VideoDirective)
290 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}