Repository navigation
Expand file tree
/
Copy pathweb_and_slides.lua
More file actions
303 lines (280 loc) · 13.8 KB
/
Copy pathweb_and_slides.lua
File metadata and controls
303 lines (280 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
-- web_and_slides.lua
-- Combined filter for dual html + revealjs Quarto modules:
-- * Div : `::: {.narration}` -> speaker notes on revealjs, plain prose elsewhere.
-- `::: {.slides-only}` -> revealjs slides only: dropped elsewhere
-- (equivalent to `::: {.content-visible when-format="revealjs"}`)
-- `::: {.html-only}` -> html only: dropped on revealjs slides
-- (equivalent to `::: {.content-hidden when-format="revealjs"}`)
-- * Pandoc : on revealjs, headings below the slide level are promoted so each
-- becomes its own slide, the slide closes after each plot (so no two
-- plots share a slide and a plot's following prose stays with the
-- next plot -- unless only prose remains before the next heading, in
-- which case it stays put rather than making a blank slide), and each
-- callout is unwrapped onto its own slide titled by its own heading
-- (no box, so any figure inside hoists and stretches; an untitled
-- callout keeps the section title; a callout the author collapsed
-- keeps its box, its body held back as a fragment, since revealjs
-- ignores Quarto's `collapse`). Split slides repeat the current
-- heading so they keep a title. Finally, any slide with 2+ cells is
-- expanded into an auto-animate build-up (one step per cell, cells
-- accumulating, notes advancing) so each step is a real slide whose
-- figure still hoists and auto-stretches; an interactive quarto-live
-- cell is skipped there, since duplicating it duplicates the editor.
-- Other formats untouched.
-- Register at the `pre-ast` stage so callouts are still plain divs here (Quarto
-- normalizes them into custom AST nodes after that point).
function Div(el)
if el.classes:includes("slides-only") then
if not quarto.doc.is_format("revealjs") then
return {} -- slides only: nothing on the website
end
return el.content
end
if el.classes:includes("html-only") then
if quarto.doc.is_format("revealjs") then
return {} -- html only: nothing on the slides
end
return el.content
end
if not el.classes:includes("narration") then
return nil -- leave every other div alone
end
if quarto.doc.is_format("revealjs") then
el.classes = el.classes:filter(function(c) return c ~= "narration" end)
el.classes:insert("notes") -- Pandoc emits <aside class="notes">
return el
end
return el.content -- drop wrapper, keep prose inline
end
function Pandoc(doc)
if not quarto.doc.is_format("revealjs") then
return nil -- leave the website (html) alone
end
local slide_level = 2 -- revealjs default
local sl = doc.meta["slide-level"]
if sl then slide_level = tonumber(pandoc.utils.stringify(sl)) or 2 end
local function is_callout(el)
for _, c in ipairs(el.classes) do
if c:match("^callout") then return true end
end
return false
end
local function has_plot(cell) -- a rendered figure/display output
for _, child in ipairs(cell.content) do
if child.t == "Div" and child.classes:includes("cell-output-display") then
return true
end
end
return false
end
local kept_collapsed = false -- a collapsed callout kept its box; needs the CSS below
-- revealjs ignores Quarto's `collapse`, so a callout the author collapsed (a
-- hidden solution, say) would otherwise be given away on the slide. Keep its
-- box and title and hold the body back as a fragment, revealed on the next
-- advance. `collapse="false"` (collapsible but open) is not affected.
local function is_collapsed(el)
local c = el.attributes["collapse"]
return c ~= nil and c ~= "false"
end
-- clone a collapsed callout with everything after its title heading wrapped in
-- a `.fragment` div; its nested content is left exactly as the author wrote it
local function hold_collapsed(el)
kept_collapsed = true
local kept, body = el:clone(), pandoc.List()
local titled = #kept.content > 0 and kept.content[1].t == "Header"
for j = (titled and 2 or 1), #kept.content do body:insert(kept.content[j]) end
if #body == 0 then return kept end
local held = pandoc.List()
if titled then held:insert(kept.content[1]) end
held:insert(pandoc.Div(body, pandoc.Attr("", { "fragment" })))
kept.content = held
return kept
end
-- unwrap callouts to plain blocks (recursively, for nested callouts) so their
-- content sits directly on the slide -- a de-boxed callout behaves like normal
-- slide content, and any figure inside hoists and auto-stretches. Headings at
-- or above the slide level are demoted so a nested title can't split the slide;
-- the leading heading is re-promoted to the slide title by the caller.
local function unwrap_callouts(blocks)
local flat = pandoc.List()
for _, b in ipairs(blocks) do
if b.t == "Div" and is_callout(b) and is_collapsed(b) then
flat:insert(hold_collapsed(b)) -- collapsed: keep the box
elseif b.t == "Div" and is_callout(b) then
flat:extend(unwrap_callouts(b.content))
elseif b.t == "Header" and b.level <= slide_level then
local h = b:clone(); h.level = slide_level + 1; flat:insert(h)
else
flat:insert(b)
end
end
return flat
end
-- is there any visible code cell before the next slide boundary, starting at
-- block index `from`? A heading, rule, or callout is a boundary (a callout
-- self-titles onto its own slide), so a trailing note only breaks if a plain
-- cell -- not a callout -- lands on the new slide first.
local function content_before_boundary(from)
for j = from, #doc.blocks do
local b = doc.blocks[j]
if b.t == "Header" or b.t == "HorizontalRule" then return false end
if b.t == "Div" and is_callout(b) then return false end
if b.t == "Div" and b.classes:includes("cell") then return true end
end
return false
end
local out = pandoc.List()
local pending = false -- a break is owed before the next slide content
local filled = false -- this slide already holds a visible cell/callout
local current_header = nil -- most recent slide title, repeated on splits
-- a break that keeps the current title: a fresh heading, else a plain rule
local function slide_break()
if current_header then
local h = current_header:clone()
h.level = slide_level -- render as a content-slide title
h.identifier = "" -- let Quarto assign a fresh unique id
return h
end
return pandoc.HorizontalRule()
end
for i = 1, #doc.blocks do
local blk = doc.blocks[i]
if blk.t == "Header" then
if blk.level > slide_level then blk.level = slide_level end -- deep headings -> own slide
if blk.level <= slide_level then pending = false; filled = false end
current_header = blk
out:insert(blk)
elseif blk.t == "HorizontalRule" then
pending = false; filled = false
out:insert(blk)
elseif blk.t == "Div" and is_callout(blk) and is_collapsed(blk) then
-- collapsed: keep the box, since unwrapping it would reveal the content.
-- Lands on its own slide under the section title, body held as a fragment.
if pending or filled then out:insert(slide_break()) end
out:insert(hold_collapsed(blk))
filled = true; pending = true
elseif blk.t == "Div" and is_callout(blk) then
-- give the callout its own slide titled by its own heading (promoted to the
-- slide level, so the heading is itself the slide boundary); content is
-- unwrapped so any figure inside hoists and stretches
local content = unwrap_callouts(blk.content)
if #content > 0 and content[1].t == "Header" then
local title = content[1]:clone()
title.level = slide_level -- callout heading -> slide title
title.identifier = ""
content[1] = title
out:extend(content)
else
if pending or filled then out:insert(slide_break()) end -- untitled callout keeps the section title
out:extend(content)
end
filled = true; pending = true -- alone; close the slide after
elseif blk.t == "Div" and blk.classes:includes("cell") then
if pending then out:insert(slide_break()); pending = false; filled = false end
filled = true
if has_plot(blk) then pending = true end -- close the slide after a plot
out:insert(blk)
else
-- prose/notes: honor a plot's pending break only if a cell/callout still
-- follows before the next heading -- so the prose introduces the next
-- slide. If only notes remain, breaking would make a blank titled slide,
-- so keep them on the current slide instead.
if pending then
if content_before_boundary(i + 1) then out:insert(slide_break()); filled = false end
pending = false
end
out:insert(blk)
end
end
-- second pass: expand any slide holding 2+ cells into an auto-animate build-up
-- -- one step per cell, cells accumulating, notes advancing with each new cell.
-- Each step is a real slide, so its figure still hoists and auto-stretches
-- (unlike a fragment). Single-cell and cell-free slides pass through unchanged.
-- a quarto-live editor cell (`{webr}` / `{pyodide}`): at this stage knitr has
-- passed the block through, so the cell holds a CodeBlock whose language is
-- still brace-wrapped
local live_langs = { -- the spellings live.lua itself accepts
["{webr}"] = true, webr = true, ["{webr-r}"] = true,
["{pyodide}"] = true, pyodide = true, ["{pyodide-python}"] = true,
}
local function is_interactive(b)
if b.t ~= "Div" or not b.classes:includes("cell") then return false end
for _, c in ipairs(b.content) do
if c.t == "CodeBlock" then
for _, cl in ipairs(c.classes) do
if live_langs[cl] then return true end
end
end
end
return false
end
-- only executed cells make build-up steps: an interactive cell is left where
-- the author put it, because duplicating it across steps duplicates the
-- editor, and whatever a student typed on one step would not carry to the next
local function is_cell(b)
return b.t == "Div" and b.classes:includes("cell") and not is_interactive(b)
end
local function strip_ids(cell) -- clone a cell with ids blanked (it is duplicated across steps)
local c = cell:clone()
c.identifier = ""
return pandoc.walk_block(c, {
Div = function(d) d.identifier = ""; return d end,
CodeBlock = function(cb) cb.identifier = ""; return cb end,
})
end
local function emit_slide(delim, body, dest)
local segs, notes = {}, pandoc.List() -- notes accumulates until the next cell
for _, b in ipairs(body) do
if is_cell(b) then
segs[#segs + 1] = { notes = notes, cell = b }; notes = pandoc.List()
else
notes:insert(b)
end
end -- leftover `notes` = trailing, after the last cell
-- nothing to build up, or no heading to carry the auto-animate flag: emit as-is
if #segs < 2 or delim == nil or delim.t ~= "Header" then
if delim then dest:insert(delim) end
for _, b in ipairs(body) do dest:insert(b) end
return
end
for k = 1, #segs do
local h = delim:clone()
h.identifier = "" -- fresh id per step
h.attributes["auto-animate"] = "true"
dest:insert(h)
for j = 1, k do dest:insert(strip_ids(segs[j].cell)) end -- cells 1..k accumulate
for _, n in ipairs(segs[k].notes) do dest:insert(n) end -- notes for the cell just revealed
if k == #segs then
for _, n in ipairs(notes) do dest:insert(n) end -- trailing notes on the last step
end
end
end
local expanded, delim, body = pandoc.List(), nil, pandoc.List()
for _, b in ipairs(out) do
-- a slide boundary is a rule or a heading at/above the slide level; deeper
-- headings (e.g. an unwrapped callout's title) stay within the slide
if b.t == "HorizontalRule" or (b.t == "Header" and b.level <= slide_level) then
emit_slide(delim, body, expanded); delim = b; body = pandoc.List()
else
body:insert(b)
end
end
emit_slide(delim, body, expanded) -- flush the last slide
-- the revealjs theme shrinks every callout box to 0.7em (and compensates for
-- that in headings). A kept box holds ordinary slide content, so put the
-- slide's own sizes back inside it -- an extra class beats the theme's
-- specificity, and the heading variables keep a custom theme's scale.
if kept_collapsed then
quarto.doc.include_text("in-header", [==[<style>
.reveal .slides section div.callout.callout-style-simple,
.reveal .slides section div.callout.callout-style-default { font-size: inherit; }
.reveal .slides section div.callout.callout-style-simple h1,
.reveal .slides section div.callout.callout-style-default h1 { font-size: var(--r-heading1-size); }
.reveal .slides section div.callout.callout-style-simple h2,
.reveal .slides section div.callout.callout-style-default h2 { font-size: var(--r-heading2-size); }
.reveal .slides section div.callout.callout-style-simple h3,
.reveal .slides section div.callout.callout-style-default h3 { font-size: var(--r-heading3-size); }
</style>]==])
end
return pandoc.Pandoc(expanded, doc.meta)
end