slide timing: per-speaker timing comments activate speaker view pacing
Port the Google-Docs slide workflow convention: timing comments like
<!-- JD 00:30 --> (speaker code plus MM:SS/HH:MM:SS/seconds duration, or
just <!-- 00:30 --> for single-speaker talks) are converted into
data-timing attributes on the rendered slides' <section> elements, and
the calculated grand total is passed to reveal.js as the totalTime
config value, activating the pacing timer in the speaker view. A
document-supplied totalTime or defaultTiming takes precedence.
A per-speaker time summary is printed during rendering; the new
slide_timing() function computes slide, speaker, and grand totals
directly from an .Rmd source file without rendering it.
Change-Id: I89d37baab06c6ee8be7ab777a70b9b21295c8780
diff --git a/tests/testthat/test-slide_timing.R b/tests/testthat/test-slide_timing.R
new file mode 100644
index 0000000..edcce26
--- /dev/null
+++ b/tests/testthat/test-slide_timing.R
@@ -0,0 +1,190 @@
+test_that("parse_duration_seconds handles all duration formats", {
+ expect_equal(parse_duration_seconds("00:30"), 30)
+ expect_equal(parse_duration_seconds("1:05"), 65)
+ expect_equal(parse_duration_seconds("45"), 45)
+ expect_equal(parse_duration_seconds("1:00:00"), 3600)
+ expect_equal(parse_duration_seconds("1:30:15"), 5415)
+ expect_true(is.na(parse_duration_seconds("abc")))
+})
+
+test_that("extract_timing_comments finds comments", {
+ lines <- c(
+ "---",
+ "title: x",
+ "---",
+ "",
+ "## Slide one",
+ "",
+ "<!-- MK 00:30 -->",
+ "",
+ "## Slide two",
+ "",
+ "<!-- AB 1:00 -->",
+ "<!-- MK 90 -->"
+ )
+ comments <- extract_timing_comments(lines)
+ expect_equal(comments$speaker, c("MK", "AB", "MK"))
+ expect_equal(comments$seconds, c(30, 60, 90))
+ expect_equal(assign_slides(lines, comments, slide_level = 2),
+ c("Slide one", "Slide two", "Slide two"))
+})
+
+test_that("comments without speaker codes are supported", {
+ lines <- c("## Slide one", "<!-- 00:30 -->", "## Slide two", "<!-- 1:05 -->")
+ comments <- extract_timing_comments(lines)
+ expect_true(all(is.na(comments$speaker)))
+ expect_equal(comments$seconds, c(30, 65))
+
+ # a plain number must not be split into a speaker code plus duration
+ comments2 <- extract_timing_comments("<!-- 45 -->")
+ expect_true(is.na(comments2$speaker))
+ expect_equal(comments2$seconds, 45)
+
+ result <- apply_timing_attributes(c(
+ "<section id='s1' class='level2'>",
+ "<!-- 00:30 -->",
+ "</section>"
+ ))
+ expect_match(result[1], "data-timing=\"30\"")
+
+ report <- timing_report_message(extract_timing_comments(lines))
+ expect_equal(report, "Slide timing -- total 1:35")
+})
+
+test_that("comments before first heading belong to title slide", {
+ lines <- c("<!-- MK 00:10 -->", "", "# Title", "", "## Slide")
+ comments <- extract_timing_comments(lines)
+ expect_equal(assign_slides(lines, comments, slide_level = 2), "(title)")
+})
+
+test_that("apply_timing_attributes adds data-timing to sections", {
+ lines <- c(
+ "<html>",
+ "<section id='title-slide'>",
+ "title",
+ "</section>",
+ "<section id='s1' class='level2'>",
+ "<!-- MK 00:30 -->",
+ "</section>",
+ "<section id='s2' class='level2'>",
+ "<!-- MK 0:20 -->",
+ "<!-- AB 00:30 -->",
+ "</section>"
+ )
+ result <- apply_timing_attributes(lines)
+ html <- paste(result, collapse = "\n")
+ expect_match(result[2], "<section id='title-slide'>")
+ expect_match(result[5], "data-timing=\"30\"")
+ # multiple speakers on one slide are summed
+ expect_match(result[8], "data-timing=\"50\"")
+ comments <- attr(result, "comments")
+ expect_equal(sum(comments$seconds), 80)
+})
+
+test_that("apply_timing_attributes is a no-op without comments", {
+ lines <- c("<section id='s1'>", "x", "</section>")
+ result <- apply_timing_attributes(lines)
+ expect_identical(as.vector(result), lines)
+ expect_equal(nrow(attr(result, "comments")), 0)
+})
+
+test_that("inject_total_time activates the pacing timer", {
+ lines <- c(
+ "var opts = {",
+ " controls: true,",
+ "};",
+ "Reveal.initialize({",
+ " controls: true,",
+ " slideNumber: true",
+ "});"
+ )
+ result <- inject_total_time(lines, 90)
+ expect_contains(result, "\t\t\ttotalTime: 90,")
+
+ # existing pacing config is respected
+ with_default <- c(lines, "", "defaultTiming: 60")
+ expect_identical(inject_total_time(with_default, 90), with_default)
+ with_total <- c("totalTime: 120", lines)
+ expect_identical(inject_total_time(with_total, 90), with_total)
+
+ # nothing to hook into -- leave the document alone
+ expect_identical(inject_total_time(c("<html>", "</html>"), 90),
+ c("<html>", "</html>"))
+})
+
+test_that("rendered presentations carry data-timing attributes", {
+ skip_if_not_pandoc()
+ skip_if_not_installed("xml2")
+ rmd <- local_temp_rmd_file(
+ "---",
+ "title: Timing test",
+ "output: revealjs.ids::revealjs_presentation",
+ "---",
+ "",
+ "## Slide A",
+ "",
+ "Content A",
+ "",
+ "<!-- MK 00:30 -->",
+ "",
+ "## Slide B",
+ "",
+ "Content B",
+ "",
+ "<!-- MK 01:00 -->",
+ "<!-- AB 0:30 -->"
+ )
+ html <- .render_and_read(rmd)
+ timings <- xml2::xml_attr(
+ xml2::xml_find_all(html, "//section[@data-timing]"),
+ "data-timing"
+ )
+ expect_equal(timings, c("30", "90"))
+ # the grand total activates the pacing timer of the notes plugin
+ expect_true(any(grepl("totalTime:\\s*120", html)))
+})
+
+test_that("a user-supplied totalTime is respected", {
+ skip_if_not_pandoc()
+ rmd <- local_temp_rmd_file(
+ "---",
+ "title: Timing test",
+ "output: revealjs.ids::revealjs_presentation",
+ "---",
+ "",
+ "## Slide A",
+ "",
+ "<!-- 00:30 -->"
+ )
+ html <- .render_and_read(rmd, output_options = list(
+ reveal_options = list(totalTime = 1800)
+ ))
+ # passed through to the config...
+ expect_true(any(grepl("totalTime:\\s*1800", html)))
+ # ... and not overwritten by the calculated total
+ expect_false(any(grepl("totalTime:\\s*30", html)))
+})
+
+test_that("slide_timing reports totals", {
+ rmd_file <- tempfile(fileext = ".Rmd")
+ writeLines(c(
+ "---",
+ "title: x",
+ "output: revealjs.ids::revealjs_presentation",
+ "---",
+ "",
+ "## Slide one",
+ "",
+ "<!-- MK 00:30 -->",
+ "",
+ "## Slide two",
+ "",
+ "<!-- AB 1:00 -->",
+ "<!-- MK 90 -->"
+ ), rmd_file)
+ expect_output(result <- slide_timing(rmd_file), "total")
+ expect_equal(result$total, 180)
+ expect_equal(result$speakers$seconds[result$speakers$speaker == "MK"], 120)
+ expect_equal(result$speakers$seconds[result$speakers$speaker == "AB"], 60)
+ unlink(rmd_file)
+})