Search code examples
quartoreveal.jsfootnotes

How to reference the same footnote more than once on a single slide?


I’m creating a Reveal.js presentation using Quarto and I want to cite the same footnote multiple times in the same slide. Here’s a minimal example (all in one .qmd file):

---
title: "test"
format: revealjs
---

## test_1

-   test_1[^1]

-   test_1[^1]

[^1]: test_1 footnote

I get this:

enter image description here

But I want to have like this:

enter image description here


Solution

  • This can be done using JavaScript. First, check all '.aside-footnotes li' list elements for dupes. If any dupe is found, it's hidden and the information of which element it is a dupe is stored. In a second pass, go over all sup elements to replace duplicated footnotes with dupe_of. A third pass checks if the newly created reused footnotes are in order:

    out

    We are treating each section seperately. It turns out, that the contents are static, but simply the class changes on scroll.

    sec

    Edit: I introduced some logic to make this work for each slide independently. Now the JS looks at each slide of class .slide.level2 and finds reused footnotes only within the scope of one slide.

    Note: This is highly customized and may create unforeseen issues. But this will give you an idea, of how it can be done.

    ---
    title: "test"
    format: revealjs
    header-includes:
      - |
        <script>
          document.addEventListener('DOMContentLoaded', function() {
          const sections = document.querySelectorAll('.slide.level2'); 
          
          sections.forEach(section => {
              const footnotes = section.querySelectorAll('.aside-footnotes li');
              if (footnotes.length === 0) return;
              
              /*
              console.log('Processing section:', section);
              console.log('Footnotes:', Array.from(footnotes).map(f => ({
                  id: f.id,
                  text: f.querySelector('p').textContent.trim()
              })));
              */
              const contentToNumber = new Map();
              let currentUniqueNumber = 1;
              
              const firstFootnoteNumber = parseInt(footnotes[0].id.match(/\d+/)[0]);
              console.log('First footnote number in section:', firstFootnoteNumber);
              
              // to correct number mapping-
              footnotes.forEach((footnote) => {
                  const footnoteText = footnote.querySelector('p').textContent.trim();
                  if (!contentToNumber.has(footnoteText)) {
                      contentToNumber.set(footnoteText, currentUniqueNumber);
                      currentUniqueNumber++;
                  }
              });
              
              // Create sequential to unique number mapping
              const numberMapping = new Map();
              footnotes.forEach((footnote, index) => {
                  const footnoteText = footnote.querySelector('p').textContent.trim();
                  const absoluteNumber = parseInt(footnote.id.match(/\d+/)[0]);
                  const sequentialNumber = absoluteNumber - firstFootnoteNumber + 1;
                  const correctNumber = contentToNumber.get(footnoteText);
                  
                  // Map sequential numbers, since footnotes count upwards accross slides :(
                  numberMapping.set(sequentialNumber, correctNumber);
                  
                  console.log(`Mapping sequential ${sequentialNumber} (absolute ${absoluteNumber}) to ${correctNumber}`);
                  
                  if (index > 0 && Array.from(footnotes)
                      .slice(0, index)
                      .some(f => f.querySelector('p').textContent.trim() === footnoteText)) {
                      footnote.style.display = 'none';
                  }
              });
              
              // Update sup elements
              const supElements = section.querySelectorAll('sup');
              supElements.forEach(sup => {
                  const currentNumber = parseInt(sup.textContent.trim());
                  const correctNumber = numberMapping.get(currentNumber);
                  
                  console.log(`Sup element: ${currentNumber} -> ${correctNumber}`);
                  
                  if (correctNumber !== undefined) {
                      sup.textContent = correctNumber.toString();
                  } else {
                      console.log(`Warning: No mapping found for sup number ${currentNumber}`);
                  }
              });
          });
          });
        </script>
    ---
    ## Footnotes
    
    -  Apple [^1]
    -  Banana [^1]
    -  Raspberry [^1]
    -  Tomato [^2]
    -  Potato [^2]
    
    
    [^1]: Fruit
    [^2]: Veggie
    
    ## Going to sleep
    
    - Get in bed [^3]
    - or even a tent [^3]
    
    [^3]: form of shelter
    
    ## combination and repetition  
    -  Apple [^1] 
    -  Banana [^1] 
    -  Raspberry [^1] 
    -  Tomato [^2] 
    -  Potato [^2] 
    - Get in bed [^3] 
    - or even a tent [^3]  
    
    [^1]: Fruit 
    [^2]: Veggie 
    [^3]: form of shelter