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/NAMESPACE b/NAMESPACE
index 7ae347d..86e0d69 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,4 +1,6 @@
# Generated by roxygen2: do not edit by hand
export(revealjs_presentation)
+export(slide_timing)
import(rmarkdown)
+importFrom(stats,aggregate)
diff --git a/NEWS.md b/NEWS.md
index b01b385..cb9f6de 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,7 @@
# revealjs.ids (development version)
+- Speaker timing comments in the source `.Rmd` (e.g. `<!-- JD 00:30 -->` for "John Doe needs 30 seconds for this slide", or just `<!-- 00:30 -->` for single-speaker talks, ported from the old Google Docs workflow) are now converted into `data-timing` attributes on the rendered slides' `<section>` elements, activating the pacing timer in the reveal.js speaker view (the calculated grand total is passed as the `totalTime` config value; a `totalTime` or `defaultTiming` set by the document itself, e.g. to start from a known time budget, takes precedence). A per-speaker time summary is printed during rendering, and the new `slide_timing()` function computes slide, speaker, and grand totals directly from an `.Rmd` file without rendering it.
+
- `ids` theme: bold ("highlighted") text now uses orange Fira Sans Condensed (600, 90% size) instead of just an orange colour in the serif body face, as in the IDS reference decks.
- `ids` theme: highlighted/code text now uses Fira Code (the monospaced Fira Sans variant, loaded from Google Fonts), as in the IDS reference decks; falls back to Fira Mono, then DejaVu Sans Mono. Replaces the generic `monospace` default.
diff --git a/R/revealjs_presentation.R b/R/revealjs_presentation.R
index 21212a7..c35cdeb 100644
--- a/R/revealjs_presentation.R
+++ b/R/revealjs_presentation.R
@@ -33,6 +33,47 @@
#' ```
#' to create notes only viewable in presentation mode.
#'
+#' ## Slide timing
+#'
+#' Speaker time can be planned per slide and per speaker by adding a comment
+#' with a speaker code and the expected duration (in `MM:SS`, `HH:MM:SS`, or
+#' plain seconds format) anywhere inside a slide:
+#'
+#' ```markdown
+#' ## My slide title
+#'
+#' Some content
+#'
+#' <!-- MK 00:30 -->
+#' ```
+#'
+#' This means Marc Kupietz will need 30 seconds for that slide. For talks
+#' presented by a single speaker, the speaker code can be omitted:
+#' `<!-- 00:30 -->`.
+#'
+#' Typically you start from a known total time budget and adjust the
+#' individual slides to it. You can set this budget yourself with the
+#' reveal.js `totalTime` option (in seconds):
+#'
+#' ```yaml
+#' output:
+#' revealjs.ids::revealjs_presentation:
+#' reveal_options:
+#' totalTime: 3600 # one hour
+#' ```
+#'
+#' During rendering, comments are converted into `data-timing` attributes on
+#' the slides' `<section>` elements (several speakers per slide are summed
+#' up), and the calculated grand total is passed to reveal.js as the
+#' `totalTime` config value. This activates the pacing timer in the
+#' [speaker view](https://revealjs.com/speaker-view/), which shows how you
+#' are doing relative to your plan. A summary of the total time per speaker
+#' is printed during rendering. If the document sets `totalTime` or
+#' `defaultTiming` itself (via `reveal_options`), those take precedence.
+#'
+#' The function [slide_timing()] computes these totals directly from an
+#' `.Rmd` source file without rendering it.
+#'
#' ### Search
#'
#' When opt-in, it is possible to show a search box when pressing `CTRL + SHIFT +
@@ -343,6 +384,21 @@
args
}
+ # post-processor converting <!-- MK 00:30 --> speaker timing comments into
+ # data-timing attributes on the enclosing sections (activating the pacing
+ # timer of the notes plugin) and reporting per-speaker totals
+ post_processor <- function(metadata, input_file, output_file, clean, verbose) {
+ lines <- readLines(output_file, warn = FALSE, encoding = "UTF-8")
+ result <- apply_timing_attributes(lines)
+ comments <- attr(result, "comments")
+ if (nrow(comments) > 0) {
+ result <- inject_total_time(result, sum(comments$seconds))
+ writeLines(enc2utf8(result), output_file, useBytes = TRUE)
+ message(timing_report_message(comments))
+ }
+ output_file
+ }
+
# return format
output_format(
knitr = knitr_options_html(fig_width, fig_height, fig_retina, keep_md),
@@ -354,6 +410,7 @@
keep_md = keep_md,
clean_supporting = self_contained,
pre_processor = pre_processor,
+ post_processor = post_processor,
base_format = html_document_base(
lib_dir = lib_dir,
self_contained = self_contained,
diff --git a/R/slide_timing.R b/R/slide_timing.R
new file mode 100644
index 0000000..560716d
--- /dev/null
+++ b/R/slide_timing.R
@@ -0,0 +1,221 @@
+# Pattern matching timing comments, e.g. <!-- MK 00:30 --> or just <!-- 00:30 -->
+timing_comment_pattern <- paste0(
+ "<!--[[:space:]]*(?:([[:alnum:]._-]+)[[:space:]]+)?",
+ "([0-9]+(?::[0-9]{2}){0,2})[[:space:]]*-->"
+)
+
+parse_duration_seconds <- function(x) {
+ vapply(x, function(t) {
+ parts <- strsplit(t, ":", fixed = TRUE)[[1]]
+ parts <- suppressWarnings(as.numeric(parts))
+ if (length(parts) == 0 || length(parts) > 3 || anyNA(parts)) {
+ return(NA_real_)
+ }
+ multipliers <- c(3600, 60, 1)[(4 - length(parts)):3]
+ sum(parts * multipliers)
+ }, numeric(1), USE.NAMES = FALSE)
+}
+
+format_seconds <- function(x) {
+ x <- round(x)
+ h <- floor(x / 3600)
+ m <- floor((x %% 3600) / 60)
+ s <- x %% 60
+ ifelse(h > 0, sprintf("%d:%02d:%02d", h, m, s), sprintf("%d:%02d", m, s))
+}
+
+# Extract all timing comments from a character vector (one line per element).
+# Returns a data frame with columns line (1-based index), speaker (NA for
+# comments without a speaker code), duration, and seconds.
+extract_timing_comments <- function(lines) {
+ matches <- regmatches(lines, regexec(timing_comment_pattern, lines))
+ hits <- which(lengths(matches) > 0)
+ if (length(hits) == 0) {
+ return(data.frame(
+ line = integer(), speaker = character(), duration = character(),
+ seconds = numeric()
+ ))
+ }
+ speaker <- trimws(vapply(matches[hits], `[[`, character(1), 2))
+ speaker[speaker == ""] <- NA_character_
+ data.frame(
+ line = hits,
+ speaker = speaker,
+ duration = vapply(matches[hits], `[[`, character(1), 3),
+ seconds = parse_duration_seconds(vapply(matches[hits], `[[`, character(1), 3)),
+ stringsAsFactors = FALSE
+ )
+}
+
+# Assign each timing comment to the slide it belongs to. A comment belongs to
+# the slide whose heading most recently appeared before it; content before the
+# first heading belongs to the title slide.
+assign_slides <- function(lines, comments, slide_level) {
+ headings <- grepl(paste0("^#{1,", slide_level, "}[[:space:]]"), lines)
+ slide_of_line <- cumsum(headings)
+ comments$slide <- slide_of_line[comments$line]
+ titles <- trimws(sub("^#+[[:space:]]+", "", lines[headings]))
+ c("(title)", titles)[comments$slide + 1]
+}
+
+#' Compute per-speaker slide times from timing comments
+#'
+#' Parses an R Markdown presentation for timing comments of the form
+#' `<!-- MK 00:30 -->` (speaker code followed by a duration in `MM:SS`,
+#' `HH:MM:SS`, or plain seconds format) or, for single-speaker talks, simply
+#' `<!-- 00:30 -->`, and computes the time allocated to each slide, each
+#' speaker, and the whole presentation.
+#'
+#' The same convention can be used directly in
+#' [revealjs_presentation()]: any such comment in the source document is
+#' automatically converted into a `data-timing` attribute on the enclosing
+#' slide's `<section>` element, which activates the pacing timer in the
+#' reveal.js speaker view.
+#'
+#' @param input Path to an R Markdown file.
+#' @param slide_level Level of heading that denotes individual slides (should
+#' match the `slide_level` used when rendering).
+#'
+#' @return A list with three elements: `slides` (one row per slide and
+#' speaker), `speakers` (total time per speaker), and `total` (grand total
+#' in seconds). `slide_timing()` is called for its side effect of printing a
+#' summary.
+#'
+#' @examples
+#' \dontrun{
+#' slide_timing("talk.Rmd")
+#' }
+#'
+#' @importFrom stats aggregate
+#' @export
+slide_timing <- function(input, slide_level = 2) {
+ lines <- readLines(input, warn = FALSE)
+ comments <- extract_timing_comments(lines)
+ if (nrow(comments) == 0) {
+ message("No timing comments found in ", input)
+ return(invisible(NULL))
+ }
+ comments$slide_title <- assign_slides(lines, comments, slide_level)
+ comments$speaker <- timing_speaker_label(comments$speaker)
+
+ by_speaker <- aggregate(seconds ~ speaker, comments, FUN = sum)
+ total <- sum(comments$seconds)
+ show_speakers <- any(comments$speaker != "(unnamed)")
+
+ cat("Slide timing for", basename(input), "\n\n")
+ if (show_speakers) {
+ per_slide <- aggregate(seconds ~ slide_title + speaker, comments, FUN = sum)
+ } else {
+ per_slide <- aggregate(seconds ~ slide_title, comments, FUN = sum)
+ }
+ print(data.frame(
+ slide = per_slide$slide_title,
+ speaker = if (show_speakers) per_slide$speaker else NULL,
+ time = format_seconds(per_slide$seconds),
+ stringsAsFactors = FALSE
+ ))
+ cat("\n")
+ named_speakers <- by_speaker[by_speaker$speaker != "(unnamed)", , drop = FALSE]
+ if (nrow(named_speakers) > 0) {
+ print(
+ data.frame(
+ speaker = c(named_speakers$speaker, "total"),
+ time = format_seconds(c(named_speakers$seconds, total)),
+ stringsAsFactors = FALSE
+ ),
+ row.names = FALSE
+ )
+ } else {
+ cat("total time:", format_seconds(total), "\n")
+ }
+
+ invisible(list(slides = comments[, c("slide_title", "speaker", "duration", "seconds")],
+ speakers = by_speaker, total = total))
+}
+
+# Display label for unnamed speakers
+timing_speaker_label <- function(speaker) {
+ ifelse(is.na(speaker), "(unnamed)", speaker)
+}
+
+# Convert timing comments into data-timing attributes on the enclosing
+# <section> elements of a rendered reveal.js HTML document. Returns the
+# modified lines invisibly along with the extracted comments as attributes.
+apply_timing_attributes <- function(lines) {
+ comments <- extract_timing_comments(lines)
+ if (nrow(comments) == 0) {
+ return(invisible(structure(lines, comments = comments)))
+ }
+
+ section_lines <- grep("^<section[^>]*>", lines)
+ section_of_comment <- findInterval(comments$line, section_lines)
+ ok <- section_of_comment > 0
+ if (!all(ok)) {
+ warning(
+ "Ignoring ", sum(!ok), " timing comment(s) outside any <section>",
+ call. = FALSE
+ )
+ comments <- comments[ok, , drop = FALSE]
+ section_of_comment <- section_of_comment[ok]
+ }
+
+ per_section <- tapply(comments$seconds, section_of_comment, sum)
+
+ for (idx in names(per_section)) {
+ target <- section_lines[as.integer(idx)]
+ lines[target] <- sub(
+ "^(<section[^>]*?)(/?>)$",
+ sprintf('\\1 data-timing="%d"\\2', round(per_section[[idx]])),
+ lines[target]
+ )
+ }
+
+ invisible(structure(lines, comments = comments))
+}
+
+# Activate the pacing timer of the notes plugin by injecting the total time
+# planned with the timing comments into the reveal.js configuration. Without
+# a totalTime or defaultTiming config value, the speaker view does not show
+# any pacing information at all.
+inject_total_time <- function(lines, seconds) {
+ # only respect explicitly configured pacing values, not e.g. the
+ # unconditional "defaultTiming: null" emitted by the pandoc template
+ pacing <- grep("\\b(totalTime|defaultTiming)\\s*:", lines, value = TRUE)
+ if (any(!grepl(":\\s*(null|0|false|\"\"|'')\\s*,?\\s*$", trimws(pacing)))) {
+ return(lines)
+ }
+ init <- grep("Reveal\\.initialize\\(\\s*\\{", lines)
+ if (length(init) == 0) {
+ return(lines)
+ }
+ append(
+ lines,
+ sprintf("\t\t\ttotalTime: %d,", round(seconds)),
+ after = init[[1]]
+ )
+}
+
+timing_report_message <- function(comments) {
+ comments$speaker <- timing_speaker_label(comments$speaker)
+ by_speaker <- aggregate(seconds ~ speaker, comments, FUN = sum)
+ total <- sum(comments$seconds)
+ named <- by_speaker[by_speaker$speaker != "(unnamed)", , drop = FALSE]
+ unnamed <- by_speaker[by_speaker$speaker == "(unnamed)", , drop = FALSE]
+ parts <- c(
+ # note: paste0() would treat a zero-length data frame column as "",
+ # so only build these strings when there is something to report
+ if (nrow(named) > 0) paste0(named$speaker, ": ", format_seconds(named$seconds)),
+ if (nrow(unnamed) > 1 || (nrow(unnamed) == 1 && nrow(named) > 0)) {
+ paste0("(unnamed): ", format_seconds(unnamed$seconds))
+ }
+ )
+ if (length(parts) > 0) {
+ sprintf(
+ "Slide timing -- %s -- total %s",
+ paste(parts, collapse = ", "),
+ format_seconds(total)
+ )
+ } else {
+ sprintf("Slide timing -- total %s", format_seconds(total))
+ }
+}
diff --git a/README.Rmd b/README.Rmd
index 089e48a..be3d9f4 100644
--- a/README.Rmd
+++ b/README.Rmd
@@ -113,6 +113,45 @@
Pressing `Esc` exits all of these modes.
+## Slide Timing
+
+You can plan the time budget of a talk by adding timing comments to your slides: a comment with an optional speaker code and the expected duration (`MM:SS`, `HH:MM:SS`, or plain seconds), ported from the old Google-Docs-based slide workflow:
+
+``` markdown
+## My slide title
+
+Some content
+
+<!-- JD 00:30 -->
+```
+
+This means John Doe will need 30 seconds for that slide. For talks presented by a single speaker, the speaker code can be omitted:
+
+``` markdown
+## My slide title
+
+Some content
+
+<!-- 00:30 -->
+```
+
+Typically you start from a known total time budget and adjust the individual slides to it. You can set this budget yourself with the `totalTime` option (in seconds):
+
+``` yaml
+output:
+ revealjs.ids::revealjs_presentation:
+ reveal_options:
+ totalTime: 3600 # one hour
+```
+
+When rendering, the comments are converted into [`data-timing` attributes](https://revealjs.com/speaker-view/) on the slides, and the calculated grand total is passed to reveal.js as the `totalTime` config value (unless you set `totalTime` yourself). This activates the pacing timer in the [speaker view](https://revealjs.com/speaker-view/) (press `s`), showing how you are doing relative to your plan. Several speakers per slide are supported, and a summary with the total time per speaker is printed:
+
+```
+Slide timing -- JD: 0:30, AB: 1:00 -- total 1:30
+```
+
+To compute the time budget without rendering the presentation, you can call `slide_timing()` directly on the `.Rmd` source file.
+
## Incremental Bullets
You can render bullets incrementally by adding the `incremental` option:
diff --git a/README.md b/README.md
index 4400c22..3b8752e 100644
--- a/README.md
+++ b/README.md
@@ -133,6 +133,57 @@
Pressing `Esc` exits all of these modes.
+## Slide Timing
+
+You can plan the time budget of a talk by adding timing comments to your
+slides: a comment with an optional speaker code and the expected
+duration (`MM:SS`, `HH:MM:SS`, or plain seconds), ported from the old
+Google-Docs-based slide workflow:
+
+``` markdown
+## My slide title
+
+Some content
+
+<!-- JD 00:30 -->
+```
+
+This means John Doe will need 30 seconds for that slide. For talks
+presented by a single speaker, the speaker code can be omitted:
+
+``` markdown
+## My slide title
+
+Some content
+
+<!-- 00:30 -->
+```
+
+Typically you start from a known total time budget and adjust the
+individual slides to it. You can set this budget yourself with the
+`totalTime` option (in seconds):
+
+``` yaml
+output:
+ revealjs.ids::revealjs_presentation:
+ reveal_options:
+ totalTime: 3600 # one hour
+```
+
+When rendering, the comments are converted into
+[`data-timing` attributes](https://revealjs.com/speaker-view/) on the
+slides, and the calculated grand total is passed to reveal.js as the
+`totalTime` config value (unless you set `totalTime` yourself). This
+activates the pacing timer in the
+[speaker view](https://revealjs.com/speaker-view/) (press `s`), showing
+how you are doing relative to your plan. Several speakers per slide are
+supported, and a summary with the total time per speaker is printed:
+
+ Slide timing -- JD: 0:30, AB: 1:00 -- total 1:30
+
+To compute the time budget without rendering the presentation, you can
+call `slide_timing()` directly on the `.Rmd` source file.
+
## Incremental Bullets
You can render bullets incrementally by adding the `incremental` option:
diff --git a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
index 608241a..7370f80 100644
--- a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
+++ b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
@@ -396,6 +396,12 @@
// Use this method for navigation when auto-sliding
autoSlideMethod: $autoSlideMethod$,
$endif$
+$if(totalTime)$
+ // The total time in seconds that is available to present all slides.
+ // This is used to show a pacing timer in the speaker view
+ totalTime: $totalTime$,
+
+$endif$
$if(defaultTiming)$
// Specify the average time in seconds that you think you will spend
// presenting each slide. This is used to show a pacing timer in the
diff --git a/man/revealjs_presentation.Rd b/man/revealjs_presentation.Rd
index c1c1ff2..ba10ef0 100644
--- a/man/revealjs_presentation.Rd
+++ b/man/revealjs_presentation.Rd
@@ -178,6 +178,46 @@
to create notes only viewable in presentation mode.
}
+}
+
+\subsection{Slide timing}{
+
+Speaker time can be planned per slide and per speaker by adding a comment
+with a speaker code and the expected duration (in \code{MM:SS}, \code{HH:MM:SS}, or
+plain seconds format) anywhere inside a slide:
+
+\if{html}{\out{<div class="sourceCode markdown">}}\preformatted{## My slide title
+
+Some content
+
+<!-- MK 00:30 -->
+}\if{html}{\out{</div>}}
+
+This means Marc Kupietz will need 30 seconds for that slide. For talks
+presented by a single speaker, the speaker code can be omitted:
+\verb{<!-- 00:30 -->}.
+
+Typically you start from a known total time budget and adjust the
+individual slides to it. You can set this budget yourself with the
+reveal.js \code{totalTime} option (in seconds):
+
+\if{html}{\out{<div class="sourceCode yaml">}}\preformatted{output:
+ revealjs.ids::revealjs_presentation:
+ reveal_options:
+ totalTime: 3600 # one hour
+}\if{html}{\out{</div>}}
+
+During rendering, comments are converted into \code{data-timing} attributes on
+the slides' \verb{<section>} elements (several speakers per slide are summed
+up), and the calculated grand total is passed to reveal.js as the
+\code{totalTime} config value. This activates the pacing timer in the
+\href{https://revealjs.com/speaker-view/}{speaker view}, which shows how you
+are doing relative to your plan. A summary of the total time per speaker
+is printed during rendering. If the document sets \code{totalTime} or
+\code{defaultTiming} itself (via \code{reveal_options}), those take precedence.
+
+The function \code{\link[=slide_timing]{slide_timing()}} computes these totals directly from an
+\code{.Rmd} source file without rendering it.
\subsection{Search}{
When opt-in, it is possible to show a search box when pressing \code{CTRL + SHIFT + F}. It will seach in the whole presentation, and highlight matched words. The
diff --git a/man/slide_timing.Rd b/man/slide_timing.Rd
new file mode 100644
index 0000000..96cdcbf
--- /dev/null
+++ b/man/slide_timing.Rd
@@ -0,0 +1,40 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/slide_timing.R
+\name{slide_timing}
+\alias{slide_timing}
+\title{Compute per-speaker slide times from timing comments}
+\usage{
+slide_timing(input, slide_level = 2)
+}
+\arguments{
+\item{input}{Path to an R Markdown file.}
+
+\item{slide_level}{Level of heading that denotes individual slides (should
+match the \code{slide_level} used when rendering).}
+}
+\value{
+A list with three elements: \code{slides} (one row per slide and
+speaker), \code{speakers} (total time per speaker), and \code{total} (grand total
+in seconds). \code{slide_timing()} is called for its side effect of printing a
+summary.
+}
+\description{
+Parses an R Markdown presentation for timing comments of the form
+\verb{<!-- MK 00:30 -->} (speaker code followed by a duration in \code{MM:SS},
+\code{HH:MM:SS}, or plain seconds format) or, for single-speaker talks, simply
+\verb{<!-- 00:30 -->}, and computes the time allocated to each slide, each
+speaker, and the whole presentation.
+}
+\details{
+The same convention can be used directly in
+\code{\link[=revealjs_presentation]{revealjs_presentation()}}: any such comment in the source document is
+automatically converted into a \code{data-timing} attribute on the enclosing
+slide's \verb{<section>} element, which activates the pacing timer in the
+reveal.js speaker view.
+}
+\examples{
+\dontrun{
+slide_timing("talk.Rmd")
+}
+
+}
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)
+})