auto-populate Overview/Überblick slides with a clickable TOC

Port the convention from the old Google Docs slide workflow: at
presentation time, any slide whose (only) heading is Overview or
Überblick gets its body content replaced with a hierarchical list of
the deck's h1/h2 slide headings, each entry linking to its slide.
Styled in the ids theme; slides without headings and the title slide
are left out.

Change-Id: I3bfd62d530f297a389030e431a6066b5adc99bb8
diff --git a/inst/reveal.js-5.2.1/dist/theme/ids.css b/inst/reveal.js-5.2.1/dist/theme/ids.css
index 8f95547..5ae8600 100644
--- a/inst/reveal.js-5.2.1/dist/theme/ids.css
+++ b/inst/reveal.js-5.2.1/dist/theme/ids.css
@@ -1054,4 +1054,34 @@
   padding: 0;
 }
 
+/*********************************************
+ * OVERVIEW / TOC SLIDES
+ *********************************************/
+.reveal .ids-overview-toc {
+  list-style: none;
+  margin: 0.2em 0 0 0;
+  padding: 0;
+  font-size: 0.72em;
+  line-height: 1.35;
+}
+.reveal .ids-overview-toc > li {
+  margin: 0.45em 0;
+  font-weight: bold;
+}
+.reveal .ids-overview-toc ul {
+  list-style: none;
+  margin: 0.1em 0 0.2em 0;
+  padding: 0 0 0 1.1em;
+  font-weight: normal;
+}
+.reveal .ids-overview-toc ul li {
+  margin: 0.18em 0;
+}
+.reveal .ids-overview-toc a {
+  color: inherit;
+}
+.reveal .ids-overview-toc a:hover {
+  color: var(--r-heading-color);
+}
+
 /*# sourceMappingURL=ids.css.map */
diff --git a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
index 651b950..a557cf6 100644
--- a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
+++ b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
@@ -876,8 +876,7 @@
 
 $if(theme-ids)$
     <footer id="ids-footer"><span>
-      $for(author)$$if(author.name)$$author.name$$else$$author$$endif$${ sep }, $endfor$ · $title$ · $date$</span></footer>
-<button id="ids-fullscreen" aria-label="Toggle fullscreen" title="Fullscreen"></button>
+      $for(author)$$if(author.name)$$author.name$$else$$author$$endif$${ sep }, $endfor$ · $title$ · $date$</span></footer><button id="ids-fullscreen" aria-label="Toggle fullscreen" title="Fullscreen"></button>
 <button id="ids-notes" aria-label="Show speaker notes" title="Speaker notes"></button>
 <div id="ids-notes-overlay" hidden>
   <header id="ids-notes-header">
@@ -977,5 +976,61 @@
   })();
 </script>
 $endif$
+<script>
+  // Slides whose (only) heading is "Overview" or "Überblick" are
+  // automatically populated with a table of contents built from the deck's
+  // h1/h2 slide headings, replacing whatever body content the slide had
+  // (ported from the Google Docs slide workflow). Styling in the ids theme.
+  (function () {
+    var TITLES = ['overview', 'überblick'];
+
+    function isOverviewSlide(slide) {
+      var h = slide.querySelector('h1,h2');
+      return !!h && TITLES.indexOf(h.textContent.trim().toLowerCase()) !== -1;
+    }
+
+    function build() {
+      if (typeof Reveal === 'undefined' || !Reveal.isReady()) return;
+      var slides = Reveal.getSlides().filter(function (slide) {
+        return !slide.classList.contains('title-frame');
+      });
+      slides.forEach(function (target) {
+        if (!isOverviewSlide(target) || target.dataset.tocDone) return;
+        target.dataset.tocDone = '1';
+        var heading = target.querySelector('h1,h2');
+        var list = document.createElement('ul');
+        list.className = 'ids-overview-toc';
+        var subList = null;
+        slides.forEach(function (slide) {
+          if (slide === target || !slide.id || isOverviewSlide(slide)) return;
+          var h = slide.querySelector('h1,h2');
+          if (!h) return;
+          var link = document.createElement('a');
+          link.href = '#/' + slide.id;
+          link.textContent = h.textContent;
+          var item = document.createElement('li');
+          item.appendChild(link);
+          if (h.tagName === 'H1') {
+            list.appendChild(item);
+            subList = null;
+          } else {
+            if (!subList && list.lastChild) {
+              subList = document.createElement('ul');
+              list.lastChild.appendChild(subList);
+            }
+            (subList || list).appendChild(item);
+          }
+        });
+        while (heading.nextSibling) target.removeChild(heading.nextSibling);
+        target.appendChild(list);
+      });
+    }
+
+    if (typeof Reveal !== 'undefined') {
+      if (Reveal.isReady()) build();
+      else Reveal.addEventListener('ready', build);
+    }
+  })();
+</script>
   </body>
 </html>