Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/extensions/quarto/video/video.lua
12923 views
1
-- === Utils ===
2
-- from http://lua-users.org/wiki/StringInterpolation
3
local interpolate = function(str, vars)
4
-- Allow replace_vars{str, vars} syntax as well as replace_vars(str, {vars})
5
if not vars then
6
vars = str
7
str = vars[1]
8
end
9
return (string.gsub(str, "({([^}]+)})",
10
function(whole, i)
11
return vars[i] or whole
12
end))
13
end
14
15
local function splitString (toSplit, delimiter)
16
delimiter = delimiter or "%s"
17
18
local t={}
19
for str in string.gmatch(toSplit, "([^".. delimiter .."]+)") do
20
table.insert(t, str)
21
end
22
return t
23
end
24
25
local function isEmpty(s)
26
return s == nil or s == ''
27
end
28
29
local isResponsive = function(width, height)
30
return isEmpty(height) and isEmpty(width)
31
end
32
33
VIDEO_SHORTCODE_NUM_VIDEOJS = 0
34
35
local VIDEO_TYPES = {
36
YOUTUBE = "YOUTUBE",
37
BRIGHTCOVE = "BRIGHTCOVE",
38
VIMEO = "VIMEO",
39
VIDEOJS = "VIDEOJS"
40
}
41
42
local ASPECT_RATIOS = {
43
["1x1"] = "ratio-1x1",
44
["4x3"] = "ratio-4x3",
45
["16x9"] = "ratio-16x9",
46
["21x9"] = "ratio-21x9"
47
}
48
49
local DEFAULT_ASPECT_RATIO = ASPECT_RATIOS["16x9"]
50
51
local wrapWithDiv = function(toWrap, aspectRatio, shouldAddResponsiveClasses)
52
local ratioClass = aspectRatio and ASPECT_RATIOS[aspectRatio] or DEFAULT_ASPECT_RATIO
53
local responsiveClasses = shouldAddResponsiveClasses and ' ratio ' .. ratioClass
54
wrapper = [[<div class="quarto-video{responsiveClasses}">{toWrap}</div>]]
55
56
return interpolate {
57
wrapper,
58
toWrap = toWrap,
59
responsiveClasses = responsiveClasses or '' }
60
end
61
62
local replaceCommonAttributes = function(snippet, params)
63
result = interpolate {
64
snippet,
65
src = params.src,
66
height = params.height and ' height="' .. params.height .. '"' or '',
67
width = params.width and ' width="' .. params.width .. '"' or '',
68
title = params.title or '',
69
ariaLabel = params.ariaLabel and ' aria-label="' .. params.ariaLabel .. '"' or '',
70
}
71
return result
72
end
73
74
local checkMatchStart = function(value, matcherFront)
75
return string.match(value, '^' .. matcherFront .. '(.-)$')
76
end
77
78
local youTubeBuilder = function(params)
79
if not (params and params.src) then return nil end
80
local src = params.src
81
match = checkMatchStart(src, 'https://www.youtube.com/embed/')
82
match = match or checkMatchStart(src, 'https://www.youtube.com/shorts/')
83
match = match or checkMatchStart(src, 'https://www.youtube%-nocookie.com/embed/')
84
match = match or checkMatchStart(src, 'https://youtu.be/')
85
match = match or string.match(src, '%?v=(.-)&')
86
match = match or string.match(src, '%?v=(.-)$')
87
88
if not match then return nil end
89
90
local YOUTUBE_EMBED = 'https://www.youtube.com/embed/'
91
params.src = YOUTUBE_EMBED .. match
92
93
local SNIPPET = [[<iframe data-external="1" src="{src}{start}"{width}{height} title="{title}"{ariaLabel} frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>]]
94
snippet = replaceCommonAttributes(SNIPPET, params)
95
96
local result = {}
97
98
result.snippet = interpolate {
99
snippet,
100
start = params.start and '?start=' .. params.start or ''
101
}
102
result.type = VIDEO_TYPES.YOUTUBE
103
result.src = params.src
104
result.videoId = match
105
106
return result
107
end
108
109
local brightcoveBuilder = function(params)
110
if not (params and params.src) then return nil end
111
local src = params.src
112
local isBrightcove = function()
113
return string.find(src, 'https://players.brightcove.net')
114
end
115
116
if not isBrightcove() then return nil end
117
118
local result = {}
119
120
local SNIPPET = [[<iframe data-external="1" src="{src}"{width}{height} allowfullscreen="" title="{title}"{ariaLabel} allow="encrypted-media"></iframe>]]
121
result.snippet = replaceCommonAttributes(SNIPPET, params)
122
result.type = VIDEO_TYPES.BRIGHTCOVE
123
result.src = params.src
124
return result
125
end
126
127
local vimeoBuilder = function(params)
128
if not (params and params.src) then return nil end
129
130
local VIMEO_STANDARD = 'https://vimeo.com/'
131
local match = checkMatchStart(params.src, VIMEO_STANDARD)
132
if not match then return nil end
133
134
-- Internal Links
135
-- bug/5390-vimeo-newlink
136
if string.find(match, '/') then
137
local internalMatch = string.gsub(match, "?(.*)", "" )
138
videoId = splitString(internalMatch, '/')[1]
139
privacyHash = splitString(internalMatch, '/')[2]
140
params.src = 'https://player.vimeo.com/video/' .. videoId .. '?h=' .. privacyHash
141
else
142
videoId = match
143
params.src = 'https://player.vimeo.com/video/' .. videoId
144
end
145
146
147
local SNIPPET = [[<iframe data-external="1" src="{src}"{width}{height} frameborder="0" title="{title}"{ariaLabel} allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>]]
148
149
local result = {}
150
151
result.snippet = replaceCommonAttributes(SNIPPET, params)
152
result.type = VIDEO_TYPES.VIMEO
153
result.src = params.src
154
result.videoId = videoId
155
156
return result
157
end
158
159
local videoJSBuilder = function(params)
160
if not (params and params.src) then return nil end
161
VIDEO_SHORTCODE_NUM_VIDEOJS = VIDEO_SHORTCODE_NUM_VIDEOJS + 1
162
local id = "video_shortcode_videojs_video" .. VIDEO_SHORTCODE_NUM_VIDEOJS
163
164
local SNIPPET = [[<video id="{id}"{width}{height} class="video-js vjs-default-skin vjs-big-play-centered {fluid}" controls preload="auto" data-setup='{}' title="{title}"{ariaLabel}><source src="{src}"></video>]]
165
local snippet = params.snippet or SNIPPET
166
snippet = replaceCommonAttributes(snippet, params)
167
snippet = interpolate {
168
snippet,
169
id = id,
170
fluid = isResponsive(params.width, params.height) and 'vjs-fluid' or ''
171
}
172
173
local result = {}
174
result.snippet = snippet
175
result.type = VIDEO_TYPES.VIDEOJS
176
result.src = params.src
177
result.id = id
178
return result
179
end
180
local getSnippetFromBuilders = function(src, height, width, title, start, ariaLabel)
181
local builderList = {
182
youTubeBuilder,
183
brightcoveBuilder,
184
vimeoBuilder,
185
videoJSBuilder}
186
187
local params = { src = src, height = height, width = width, title = title, start = start, ariaLabel = ariaLabel }
188
189
for i = 1, #builderList do
190
local builtSnippet = builderList[i](params)
191
if (builtSnippet) then
192
return builtSnippet
193
end
194
end
195
end
196
197
local helpers = {
198
["checkMatchStart"] = checkMatchStart,
199
["youTubeBuilder"] = youTubeBuilder,
200
["brightcoveBuilder"] = brightcoveBuilder,
201
["vimeoBuilder"] = vimeoBuilder,
202
["videoJSBuilder"] = videoJSBuilder,
203
["wrapWithDiv"] = wrapWithDiv,
204
["VIDEO_TYPES"] = VIDEO_TYPES,
205
["VIDEO_SHORTCODE_NUM_VIDEOJS"] = VIDEO_SHORTCODE_NUM_VIDEOJS,
206
["getSnippetFromBuilders"] = getSnippetFromBuilders
207
}
208
209
-- makes an asciidoc video raw block
210
-- see https://docs.asciidoctor.org/asciidoc/latest/macros/audio-and-video/
211
function formatAsciiDocVideo(src, type)
212
return 'video::' .. src .. '[' .. type .. ']'
213
end
214
215
local function asciidocVideo(src, height, width, title, start, _aspectRatio, ariaLabel)
216
local asciiDocVideoRawBlock = function(src, type)
217
return pandoc.RawBlock("asciidoc", formatAsciiDocVideo(src, type) .. '\n\n')
218
end
219
220
local videoSnippetAndType = getSnippetFromBuilders(src, height, width, title, start, ariaLabel)
221
if videoSnippetAndType.type == VIDEO_TYPES.YOUTUBE then
222
-- Use the video id to form an asciidoc video
223
if videoSnippetAndType.videoId ~= nil then
224
return asciiDocVideoRawBlock(videoSnippetAndType.videoId, 'youtube');
225
end
226
elseif videoSnippetAndType.type == VIDEO_TYPES.VIMEO then
227
return asciiDocVideoRawBlock(videoSnippetAndType.videoId, 'vimeo');
228
elseif videoSnippetAndType.type == VIDEO_TYPES.VIDEOJS then
229
return asciiDocVideoRawBlock(videoSnippetAndType.src, '');
230
else
231
-- this is not a local or supported video type for asciidoc
232
-- we should just emit a hyper link
233
end
234
235
end
236
237
function htmlVideo(src, height, width, title, start, aspectRatio, ariaLabel)
238
239
-- https://github.com/quarto-dev/quarto-cli/issues/6833
240
-- handle partially-specified width, height, and aspectRatio
241
if aspectRatio then
242
-- https://github.com/quarto-dev/quarto-cli/issues/11699#issuecomment-2549219533
243
-- we remove quotes as a
244
-- local workaround for inconsistent shortcode argument parsing on our end.
245
--
246
-- removing quotes in general is not a good idea, but the
247
-- only acceptable values for aspectRatio are 4x3, 16x9, 21x9, 1x1
248
-- and so we can safely remove quotes in this context.
249
local strs = splitString(aspectRatio:gsub('"', ''):gsub("'", ''), 'x')
250
local wr = tonumber(strs[1])
251
local hr = tonumber(strs[2])
252
local aspectRatioNum = wr / hr
253
if height and not width then
254
width = math.floor(height * aspectRatioNum + 0.5)
255
elseif width and not height then
256
height = math.floor(width / aspectRatioNum + 0.5)
257
end
258
end
259
260
local videoSnippetAndType = getSnippetFromBuilders(src, height, width, title, start, ariaLabel)
261
local videoSnippet
262
263
videoSnippet = videoSnippetAndType.snippet
264
265
if (videoSnippetAndType.type == VIDEO_TYPES.VIDEOJS) then
266
-- Can this be bundled with the VideoJS dependency
267
-- Avoid disjointed combination?
268
quarto.doc.add_html_dependency({
269
name = 'videojs',
270
scripts = { 'resources/videojs/video.min.js' },
271
stylesheets = { 'resources/videojs/video-js.css' }
272
})
273
local id = videoSnippetAndType.id or ''
274
local scriptTag = "<script>videojs(" .. id .. ");</script>"
275
quarto.doc.include_text("after-body", scriptTag)
276
end
277
278
local isVideoJS = function()
279
return videoSnippetAndType.type == VIDEO_TYPES.VIDEOJS
280
end
281
282
local isRevealJS = function()
283
return quarto.doc.is_format('revealjs')
284
end
285
286
local shouldAddResponsiveClasses = false
287
if isResponsive(width, height)
288
and not isRevealJS()
289
and not isVideoJS() then
290
if (not quarto.doc.has_bootstrap()) then
291
quarto.doc.add_html_dependency({
292
name = 'bootstrap-responsive',
293
stylesheets = { 'resources/bootstrap/bootstrap-responsive-ratio.css' }
294
})
295
end
296
shouldAddResponsiveClasses = true
297
end
298
299
if not isRevealJS() then
300
videoSnippet = wrapWithDiv(
301
videoSnippet,
302
aspectRatio,
303
shouldAddResponsiveClasses
304
)
305
end
306
307
-- inject the rendering code
308
return pandoc.RawBlock('html', videoSnippet)
309
end
310
-- return a table containing shortcode definitions
311
-- defining shortcodes this way allows us to create helper
312
-- functions that are not themselves considered shortcodes
313
return {
314
["video"] = function(args, kwargs, _meta, raw_args)
315
checkArg = function(toCheck, key)
316
value = pandoc.utils.stringify(toCheck[key])
317
if not isEmpty(value) then
318
return value
319
else
320
return nil
321
end
322
end
323
324
local srcValue = checkArg(kwargs, 'src')
325
local titleValue = checkArg(kwargs, 'title')
326
local startValue = checkArg(kwargs, 'start')
327
local heightValue = checkArg(kwargs, 'height')
328
local widthValue = checkArg(kwargs, 'width')
329
local aspectRatio = checkArg(kwargs, 'aspectRatio')
330
local ariaLabelValue = checkArg(kwargs, 'aria-label')
331
332
if isEmpty(aspectRatio) then
333
aspectRatio = checkArg(kwargs, 'aspect-ratio')
334
end
335
336
if isEmpty(srcValue) then
337
338
if #raw_args > 0 then
339
srcValue = pandoc.utils.stringify(raw_args[1])
340
else
341
-- luacov: disable
342
fail("No video source specified for video shortcode")
343
-- luacov: enable
344
end
345
end
346
347
if quarto.doc.is_format("html:js") then
348
return htmlVideo(srcValue, heightValue, widthValue, titleValue, startValue, aspectRatio, ariaLabelValue)
349
elseif quarto.doc.is_format("asciidoc") then
350
return asciidocVideo(srcValue, heightValue, widthValue, titleValue, startValue, aspectRatio, ariaLabelValue)
351
elseif quarto.doc.is_format("markdown") then
352
if srcValue:sub(1, 4) == "http" then
353
-- For remote videos, we can emit a link
354
return pandoc.Link(srcValue, titleValue or srcValue)
355
else
356
-- For local
357
-- use an image to allow markdown previewers to show video
358
return pandoc.Image(quarto.utils.as_inlines(titleValue), srcValue)
359
end
360
else
361
-- Fall-back to a link of the source
362
return pandoc.Link(srcValue, srcValue)
363
end
364
365
end,
366
["video-helpers"] = helpers,
367
}
368
369