Is there a way to remove footnotes from the outline?

In some of my figures, I have footnotes in the captions. Now I want to create a “Table of figures”, however, it seems the footnote will then also appear in the outline.
Is there a way, to remove the footnote from the outline, while still keeping it on the page of the figure?

Here is a piece of code, that recreates my issue.

#outline(
  title: [Table of figures],
  target: figure.where(kind: image),
  //fill: none,
)
#pagebreak()
#figure(
  image("placeholder.png"),
  caption:[Test. #footnote[This appears on the 'Table of Figures' page as well.]]
)

Hi there! Welcome to the Forum.
This seems to be a bug that is on the radar already, as seen in this issue:
Until the error is fixed, you can do the following:

#[
#show footnote.entry: hide
#show ref: none
#show footnote: none
#outline(title: [Table of figures],
target: figure.where(kind: image),
//fill: none,
)
]
#pagebreak()
#counter(footnote).update(0)
#figure(
image("placeholder.png"),
caption:[Test. #footnote[This appears on the ‘Table of Figures’ page as well.]]
)

If there are any questions left, don’t hesitate to ask again.

1 Like

The example fails when #pagebreak() is removed.

The ’ “clean” solution’ from the github issue should work:

#let show-footnote = state("show-footnote", true)
#let footnote(..args) = context if show-footnote.get() { std.footnote(..args) }

#let clean-footnote(it) = context {
  let origin-value = show-footnote.get()
  context (show-footnote.update(false) + it)
  show-footnote.update(origin-value)
}

#show outline: clean-footnote

#outline(target: figure.where(kind:image))

#pagebreak()
#figure(
image("placeholder.png"),
caption:[Test. #footnote[This appears on the ‘Table of Figures’ page as well.]]
)

This works, but is there a reason for the nested context invocations?

In my testing it works just fine with

context (show-footnote.update(false) + it)

replaced by

show-footnote.update(false) + it

but either way I now get complaints of the layout failing to converge after 5 attempts, which the handy help suggests may be related to state().

To be honest, I just copied the code from Github and tested it, so I don’t know if the context is needed.
It compiles for me in the browser with Typst version 1.13.1: Typst
If you modified the code, please share it here so I can help you further.

Sure. I changed this:

#let show-footnote = state("show-footnote", true)
#let footnote(..args) = context if show-footnote.get() { std.footnote(..args) }

#let clean-footnote(it) = context {
  let origin-value = show-footnote.get()
  context (show-footnote.update(false) + it)
  show-footnote.update(origin-value)
}

to this:

#let show-footnote = state("show-footnote", true)
#let footnote(..args) = context if show-footnote.get() { std.footnote(..args) }

#let clean-footnote(it) = context {
  let origin-value = show-footnote.get()
  show-footnote.update(false) + it)
  show-footnote.update(origin-value)
}

That is, I removed the context around the show-footnote.update(false) + it.

As I said, it works just as well in my document, but does lead to a failure to converge complaint.

This is because that second occurrence of context was redundant. It is already provided before.

EDIT: Context is already available. Now, is it a good thing to remove the second occurrence? I haven’t looked at the code. May be wise to look at Nested Contex - Typst Documentation

Context blocks can be nested. Contextual code will then always access the innermost context.

Apologies for the wrong link :sweat_smile: . Apparently it did not copy it and I did not notice because I had another link in the clipboard. I corrected the link now.

So as I said, it works with and without context for me as demonstrated in the shared document.

But I noticed that you did not delete the outer bracket which was used for the context:

show-footnote.update(false) + it)
But that should give you the error message Unclosed delimiter

Yes, I saw that nested contexts may be useful. That is why I asked why it is used here, since I see no benefit in this case.

The spurious closing parenthesis was an editing error in my reply. I do not have it in my working code, and thus no error message.

There is an existing question about debugging converge complaint errors on this forum, but I do not understand why it works on my end. Are you sure you are using the same code? Are you using version 0.13.1? And how/where are you running it?

That context does indeed look redundant to me. show-footnote.update(false) itself doesn’t change when context is provided, and the it (at least as fas as footnote is concerned) contains its own context. In any case, for the context you removed to matter, it would have to come after the update:

show-footnote.update(false) + context it

Well, the proposed fix is unfortunately an instance of a common antipatttern regarding state updates; see Why is State Final not "final"? - #2 by SillyFreak. If I had to guess, other people testing this (including @Adrian_Weitkemper) have only one or two such footnotes in their document, and you at least four or five. I’d say that it probably disappears if you only have three or fewer footnotes like this. If that’s the case, I think it would be valuable for you to point it out in the issue (if you have a Github account).


The original reason for using

let origin-value = show-footnote.get()
...
show-footnote.update(origin-value)

Is to reset the value correctly even if you don’t know the previous value:

  • show-footnote starts as true
  • you enter clean-footnote, it should be false in there
  • you exit clean-footnote, now it should be true again

but what if you call clean-footnote inside clean-footnote? then the inner one should not set show-footnote back to true! Thus the use of the antipattern.

One solution is to use a stack for the state:

let show-footnote = state("show-footnote", (true,))

let clean-footnote(it) = context {
  show-footnote.update(stack => (..stack, false))
  it
  show-footnote.update(stack => stack.slice(0, -1))
}

This way the previous state is preserved inside the state instead of in a layout iteration.

Thank you for that. This works both to suppress the footnotes and to eliminate the failure to converge after changing:

#let footnote(..args) = context if show-footnote.get() { std.footnote(..args) }

to

#let footnote(..args) = context if show-footnote.get().last() { std.footnote(..args) }

The final snippet is

#let show-footnote = state("show-footnote", (true,))
#let footnote(..args) =  context  if show-footnote.get().last() { std.footnote(..args) }

#let clean-footnote(it) = context {
  show-footnote.update(stack => (..stack, false))
  it
  show-footnote.update(stack => stack.slice(0, -1))
}
#show outline: clean-footnote

#outline(target...
1 Like

The example works for removing the footnote in the outline, but it is still shown at the bottom of the page.

Is it possible to also remove the footnote in the footer on that specific page?

Furthermore the counter starts at 1. That does make sense, when you think of the footnote as the “first” in the document, since it is visible in the outline. But when you now hide it in the outline, it should not be “number 1”, since it is not the first anymore.