Quarto cross-references: persistent cross-references when text changes
Asked Answered
A

2

7

I am creating a long PDF report using Quarto. I want to include two formats of cross-reference:

  1. To the section/figure number, e.g.

As discussed in Section 3.1

  1. To the section/figure title, e.g.

As discussed in My awesome section.

It is possible to achieve either like this:

---
format:   
  pdf:
    number-sections: true
---

# Why Stack Overflow is so great {#sec-stack}

I can use style 1, the section number, with the explicit ID, e.g. as seen in @sec-stack.

I can use style 2, the section title, with a link [Why Stack Overflow is so great].

I can also custom text with [Custom text before the link][Why Stack Overflow is so great].

This produces the desired output:

enter image description here

Problem

The problem is that the document is being redrafted by several authors. If a section title is changed from Why Stack Overflow is so great to Why I love Stack Overflow, it breaks the cross-references using the second style (section title).

enter image description here

I am looking for a way to refer to a section using the explicit identifier @sec-stack, and have it display the title instead of the section number. This would be something like [@sec-stack] rather than @sec-stack.

There are various options in the Quarto cross-referencing docs. However, I can't see a way to make it so that cross-reference text updates if the section title is updated, provided the explicit identifier remains the same.

Does this exist?

Annunciate answered 10/1, 2023 at 14:5 Comment(0)
R
5

I have written a Lua filter name_crossref.lua to get the cross-reference text instead of section/figure/table number. And the following approach works for both HTML and pdf output format.

Usage:

  1. For section or subsection title, its use is very simple. We just need to use \nameref{sec-id} where sec-id is the section identifier used in headers as #sec-id

  2. For images or tables generated from code chunks, we need to use some chunk options. we need to use link as code chunk class, need to define an id for cross-referring the image or table later with link-id, and need to define a title that will be used as cross-reference text with link-title. And then use the id that we have assigned to link-id to refer the generated table/image using \nameref{id}.

  3. For images added with markdown syntax or markdown tables, the use of this filter is a bit hacky. We need to use nested divs (created with pandoc div syntax, :::: for outer div and ::: for inner div). In the first div, we have to add the link class, link-id, link-title, and in the second div we need to add another class cell-output-display. And similarly, then use the id that we have assigned to link-id to refer to the generated table/image using \nameref{id}.

---
title: "Cross Referencing the Name"
author: Shafee
format: 
  html: default
  pdf: default
number-sections: true
filters: 
  - name_crossref.lua
---

# Why Quarto is so great {#sec-stack}

`r stringi::stri_rand_lipsum(1)`
See \nameref{sec-stack}.

## How it is so {#how}

`r stringi::stri_rand_lipsum(1)`
See \nameref{how}.

## Images

```{r}
#| classes: link
#| link-id: fig1
#| link-title: My Awesome plot

plot(1:10)
```

`r stringi::stri_rand_lipsum(1)`
see \nameref{fig1}

## Tables

```{r}
#| classes: link
#| link-id: tab1
#| link-title: Mtcars Data

head(mtcars)
```

`r stringi::stri_rand_lipsum(1)`
see \nameref{tab1}


# Markdown Images

:::: {.link link-id="fig2" link-title="Scatter plot of mpg"}
::: {.cell-output-display}

![mpg](test-filename/mpg.png)
:::
::::

`r stringi::stri_rand_lipsum(4)`

see \nameref{fig2}


# Markdown Table

:::: {.link link-id="tab2" link-title="Markdown table"}
::: {.cell-output-display}


| Col1 | Col2 | Col3 |
|------|------|------|
| A    | B    | C    |
| E    | F    | G    |
| A    | G    | G    |

: My Caption

:::
::::

`r stringi::stri_rand_lipsum(4)`

see \nameref{tab2}

name_crossref.lua

local str = pandoc.utils.stringify

function get_header_data(data)
  local get_headers = {
      Header = function(el)
        local id = el.identifier
        local text = str(el.content):gsub("^[%d.]+ ", "")
        table.insert(data, {id = id, text = text})
      end,
      
      Div = function(el)
        if el.attributes["link-id"] then
          local id = el.attributes["link-id"]
          local text = el.attributes["link-title"]
          table.insert(data, {id = id, text = text})
        end
      end
    }
  return get_headers
end

function change_ref(data)
  local change_rawinline = {
    RawInline = function(el)
      for key, value in pairs(data) do
        if el.text:match("\\nameref{(.*)}") == value.id then
          local target =  "#" .. value.id 
          local link = pandoc.Link(value.text, target)
          return link
        end
      end
    end
  }
  return change_rawinline
end

local function add_div_id(div)
  return {
   Div = function(elem)
     if elem.classes:includes("cell-output-display") 
      or elem.classes:includes("cell-output-stdout")then
      elem.identifier = div.attributes["link-id"]
      return elem
     end
  end
  }
end

function Div(el)
  if el.classes:includes('link') then
    return el:walk(add_div_id(el))
  end
end

function Pandoc(doc)
  local header_data = {}
  doc:walk(get_header_data(header_data))
  return doc:walk(change_ref(header_data))
end

First part of the generated output

First part of the generated output

Randolf answered 12/1, 2023 at 13:36 Comment(4)
Now the question is, whether it is useful to be a Quarto filter ???Randolf
Hi shafee I won't get the chance to try this and accept the answer until later but it looks awesome - thank you. What would the advantage of it being a Quarto filter rather than a Lua script be? It looks easy to include the filter in the yaml.Annunciate
Quarto filter is actually based on Lua filter. So not much difference and advantage I think. If you look into the quarto doc you would get the idea :).Randolf
OK I've tried it out for both html and pdf - this is really awesome. I would up-vote twice if I could. Firstly it works perfectly for both formats. Secondly it has opened my eyes to a whole new world of Lua filters. Perhaps you should make this a Quarto filter/extension (still not entirely clear on the difference) so it can go on their website Quarto is still maturing as a technology and I'm sure many would benefit from a filter like this. Plus if the developers see it they might think about making it a core feature.Annunciate
A
3

After some experimentation, this is a a LaTeX approach based on this TeX SE answer.

We load the nameref package in the header, and then replace change the Quarto references from @sec-stack to \nameref{sec-stack}. The reference persists after changing the section title, e.g. to Why I love Stack Overflow:

---
format:   
  pdf:
    number-sections: true
header-includes:
 \usepackage{nameref}
---

# Why I love Stack Overflow {#sec-stack}

I can use style 1, the section number, with the explicit ID, e.g. as seen in @sec-stack.

I can use style 2, the section title, with a link \nameref{sec-stack}.

However

The disadvantage is that this is not a native Quarto solution, and will not render to other formats e.g. HTML. However, it works and it's a simple one-off regex replace to change all the references. I have not accepted this answer and if there is a native Quarto answer I will accept that.

Annunciate answered 10/1, 2023 at 15:43 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.