How to create a chapter edge index / thumb index?

EDIT: Solution in my last reply.

I’m trying to create a chapter thumb index on the right side of the page. I want each page to display a little box on the side with the chapter number, so that the reader of the book has a visual clue where to open the book when it’s closed.

How can I manage to automatically set the chapter number as well as the shift the index block downwards on the page using the dy argument? Essentially, I believe I need to query the state of the headings in their final state to obtain the total number of headings. Using this number, I should be able to calculate the vertical offset of the first chapter index so that all index markers are distributed around the vertical center of the page. Unfortunately, I haven’t found a way to implement this yet.

This is the code that I have so far. However, I’m having issues retrieving the heading counter for the headings obtained from the query. Thanks for you help in advance!

#set page(
  background: context{
  let headings_before = query(heading.where(level: 1).before(here()))
  let headings_after = query(heading.where(level: 1).after(here()))
  let number = none
  if headings_before.len() == 0 {
    // this line doesn't work, why?
    number = counter(headings_after.first()).get().first()
  } else {
    let page = here().page()
    let page_next = headings_after.first().location().page()
    if page_next == page {
      // this line doesn't work, why?
      number = counter(headings_after.first()).get().first()
    } else {
      // this line doesn't work, why?
      number = counter(headings_before.first()).get().first()
    }
  }
  place(
    horizon + right,
    block(width: 1cm, height: 1cm, fill: gray)[
      #set align(center)
      #text(fill: white, weight: "bold", size: 25pt)[#number]
    ]
  )
}
)

When you say “this line doesn’t work, why?”, what is the error message? What’s the expected vs actual behaviour?

The error you should have gotten is that you are passing content, but the expected type is function/label/selector/etc.

tl;dr: the counter type expects a selector and will do the query. So just pass your selector instead.

let number = counter(heading.where(level: 1).after(here())).get().first()

Thanks, that helps! However, when I use after, it just gives me (0, ). When I use before instead, it works fine.

#context{
  counter(heading.where(level: 1).after(here())).get()
}

(0,) is an array, I suppose the result of get(), then you need to pick an item out of that array. Hence you use .get().first(). get() returns you the results of the counter.

I understand that it is an array and that I need to get the first element, which is supposed to be my heading number. The issue is that this number is always 0, even if there are multiple first level headings before and after the querying of the counter. On the other hand, using before instead of after for the selector will correctly return the actual number of the previous heading, not 0.

Let me give a clear example of the issue:

Test code:

#set heading(numbering: "1.1")
= Heading 1
= Heading 2

This is the number of the previous heading: 
#context{
  counter(selector(heading).before(here())).get().first()
}

This is the number of the following heading (wrong):
#context{
  counter(selector(heading).after(here())).get().first()
}

= Heading 3
= Heading 4

Output:
grafik

1 Like

The counter you want to use is probably just counter(heading). Using a custom selector instead just creates a completely different counter which is not stepped automatically.

To get the number of a specific heading, you can then use the at method of the counter as in

#let next-heading = query(selector(...)).first()
#let number = counter(heading).at(next-heading.location())
2 Likes

Thank you so much for your help, this solved the issue for me! Here is my full code for the thumb index implemented with your solution, in case anybody needs it in the future.

#let thumb-index(height: 1cm, width: 1cm) = context{
  let total-headings = counter(heading.where(level: 1)).final().first()
  let previous-headings = query(selector(heading).before(here()))
  let next-headings = query(selector(heading).after(here()))
  let number = none
  if previous-headings.len() == 0 and next-headings.len() == 0 {
    // if there are no headings in the document, return none
    return
  }
  if next-headings.len() == 0{
    // if there are no more headings after, the numbering continues indefinitely
    number = counter(heading).at(previous-headings.last().location()).first()
  } else if previous-headings.len() == 0 {
    // if there are no previous headings, use the first heading found
    number = counter(heading).at(next-headings.first().location()).first()
  } else {
    let current-page = here().page()
    let next-heading-page = next-headings.first().location().page()
    if next-heading-page == current-page {
      // if the next heading is on the current page, use the next heading's number
      number = counter(heading).at(next-headings.first().location()).first()
    } else {
      // otherwise (general case) use the previous heading's number
      number = counter(heading).at(previous-headings.last().location()).first()
    } 
  }
  // vertical offset for the thumb markers
  let offset = (number - 1 - (total-headings - 1) / 2) * height
  
  place(
    horizon + right,
    dy: offset,
    block(width: width, height: height, fill: gray)[
      #set align(center)
      #text(fill: white, weight: "bold", size: 25pt)[#number]
    ]
  )
}

#set page(
  background: thumb-index(height: 3cm)
)
1 Like