How to keep a consistent figure caption and outline after changing the style of the figure caption?

I am trying to write the supporting information of the paper by using typst. The code is provided as follows.

However, the format of the outline is not consistent with figure captions, for example:

  • Figure 1: in the outline, but Figure S1. in the figure caption.
  • Normal font of Figure 1: in the outline, but bold in the figure caption. I want the Figure 1: in the outline changed into a bold font as that in the figure caption.

Thanks for your help.

#set page("a4", margin: auto)

// Long and short captions for the outline
#let in-outline = state("in-outline", false)
#show outline: it => {
  in-outline.update(true)
  it
  in-outline.update(false)
}
#let ffcaption(long, short) = context if in-outline.get() { short } else { long }

// table
#show table.cell.where(y: 0): strong

// Change figure caption from Figure 1: to *Figure 1.*
#show figure.caption: it => [
    #text(weight: "bold")[
        #it.supplement S#it.counter.display(it.numbering).
    ~]#it.body
]

// make tabel caption on the top
#show figure.where(
    kind: table
): set figure.caption(position: top)

// outline for image and table
#outline(
    target: figure.where(kind: image).or(figure.where(kind: table)),
)

// figure
#figure(
    rect(height:6cm, width: 11cm, fill: red),
    caption: ffcaption([A curious figure.], [Short1]),
) <fig1>


#figure(
    rect(height:10cm, width: 5cm, fill: blue),
    caption: ffcaption([A curious figure.], [Short2 for]),
) <fig2>


#figure(
    table(
        columns: 2,
        stroke: (_, y) => (
            top: if y <= 1 { 1pt } else { 0pt },
            bottom: 1pt,
        ),
        [*Amount*], [*Ingredient*],
        [360g], [Baking flour],
        [Drizzle], [Vanilla extract],
    ),
    caption: ffcaption([a caption],[Table short]),
) <tbl1>


#figure(
    table(
        columns: 2,
        stroke: (_, y) => (
            top: if y <= 1 { 1pt } else { 0pt },
            bottom: 1pt,
        ),
        [*Amount*], [*Ingredient*],
        [360g], [Baking flour],
    ),
    caption: ffcaption([a caption with very very very much much long long long long of of of new new new text is o with out a n ew c],
        [table, short2])
) <tbl2>

I found I must restyle the content of the outline by myself. I found the following solutions, although I do not fully understand the following script.

// from: https://github.com/typst/typst/discussions/4515
#show outline.entry: it=> if <no-prefix> != it.fields().at(
    default: {},
    "label",
) [#outline.entry(
    it.level,
    it.element,
    {
        [*#it.element.caption.supplement *]
        [*#numbering(
            it.element.numbering,
            ..it.element.counter.at(it.element.location()),
        )*]
        [*.* ]
        // figure.caption.separator
        it.element.caption.body
    },
    it.fill,
    [#it.page]
)<no-prefix>] else {it}

Hi @chxp, if the solution you found was sufficient to you, feel free to mark your reply as the accepted answer. It makes it easier for future readers to find the solution.


Here’s an explanation that hopefully makes sense to you. First, I tried to “clean up” (which is subjective of course) the code and try to bring both the caption and outline styling in a similar form, so that it’s easier to compare them.

Here they are:

#show figure.caption: it => {
  text(weight: "bold", {
    [#it.supplement S]
    context it.counter.display(it.numbering)
    [.~]
  })
  it.body
}

#show outline.entry: it => {
  let entry-label = it.fields().at("label", default: none)
  if entry-label == <no-prefix> {
    return it
  }

  let elem = it.element
  let new-entry = outline.entry(
    it.level,
    it.element,
    {
      text(weight: "bold", {
        [#it.element.caption.supplement S]
        numbering(it.element.numbering, ..it.element.counter.at(it.element.location()))
        [.~]
      })
      // figure.caption.separator
      it.element.caption.body
    },
    it.fill,
    [#it.page]
  )
  [#new-entry<no-prefix>]
}

The cleanup mostly consisted of

  • using {...} instead of [...] in a few places so that whitespace doesn’t change the result
    • thus also breaking some of the code over multiple lines
  • use variables entry-label, elem and new-entry to make the code “flatter”

Both snippets now contain a text(weight: "bold", {...}) part that is in charge of the supplement and numbering; I assume that’s not the problem, but the surrounding code.

This code contains a show rule for outline.entry that creates a new outline.entry when it runs. Without any countermeasures, this would lead to infinite recursion: There is a new entry? Run the show rule! Now there’s an even newer entry, so run the show rule again!

To solve this, the show rule must recognize whether it is processing an entry that it itself created. The following code does this:

// get the label of the entry. a regular entry won't have one
let entry-label = it.fields().at("label", default: none)
// if the label is present, return the entry unmodified, preventing another run of the show rule
if entry-label == <no-prefix> {
  return it
}

let new-entry = ...
// return the new entry, but first attach a label to it
[#new-entry<no-prefix>]

(the old code had a big if ... [...] else {it} that achieved the same, but this way I think it’s more obvious what the return it is connected to)