How to label an element in a function from outside the function?

I wrote a function that allows to add a source to a figure.

#let fig(
  caption: [], 
  source: [],
  show_source: true,
  label: [],
  fig
) = [
  
  #show figure.caption: it => {
    if show_source {
      if it.kind == "i-figured-image" and source == [] {
        it + footnote([Quelle: Eigene Darstellung])
      } else {
        it + footnote([Quelle: #source])
      }
    } else {
      it
    }
  } 

  #figure(caption: caption, fig) #label
]

However, I couldn’t find a good solution to label this figure. The best I came up with was to pass the label to the function. However, for consistency, I`d like to add the label to the figure like this when calling the function:

#fig(...) <mylabel>

Is this possible?

It seems to me that you should be able to put a label after the function call like #fig(...) <mylabel>. What behavior isn’t working as you intend when you do this compared to passing in a label to the function?

The problem is that fig() doesn’t return a figure, it returns the following (displayed via repr(fig(...)), but I simplified fig() a bit for this):

The actual code
#let fig(
  caption: [],
  source: [],
  show_source: true,
  // label: [],
  fig
) = {
  show figure.caption: it => {
    if show_source {
      if it.kind == "i-figured-image" and source == [] {
        it + footnote([Quelle: Eigene Darstellung])
      } else {
        it + footnote([Quelle: #source])
      }
    } else {
      it
    }
  }

  figure(caption: caption, fig)
}

#fig([foo]) <mylabel>

#repr(fig([foo]))
styled(child: figure(body: [foo], caption: caption(body: [])), ..)

the label would attach to that, which means that referencing the figure via the label wouldn’t work. (@gabe, this is probably why you didn’t notice it not working: the labelling is not what fails.)

Unfortunately I’m not aware of a way to work around that, but I’d be interested in one as well.

2 Likes

EDIT: Forget it, just realized it still not working as expected.

As @SillyFreak said, you are not returning a Figure which you can attach, but a styled object instead.

One way to overcome this problem is to put the show rule outside of the fig function, and use a combination of state and metadata to know if and what value pass to the figure caption to create the footnote.

#set page(height: auto)

#let has-source = state("has-source", false)

#show figure.caption: it => context {
  let show-source = has-source.get()
  if show-source {
    let source-metadata = query(selector(<source>).before(here()))
    let source = source-metadata.last().value
    if it.kind == "i-figured-image" and source == [] {
      it + footnote([Quelle: Eigene Darstellung])
    } else {
      it + footnote([Quelle: #source])
    }
  } else {
    it
  }
  has-source.update(false)
}

#let fig(
  caption: [], 
  source: [],
  fig
) = {
  has-source.update(true)
  [#metadata(source) <source>]
  figure(caption: caption, fig)
}

#fig(
  caption: "A rect with a source",
  source: "The Typst rect function",
  rect()
) <fig:rect>

#figure(
  caption: "A rect without source",
  rect()
)

#fig(
  caption: "A yellow rect with a source",
  source: "This one is yellow",
  rect(fill: yellow)
)
Result

fig-with-source

In this solution, the has-source state is set to true inside the fig function before the figure is constructed. As a result, when the show rule of the caption gets the value of the state, it will only be true if you called fig instead of figure. When this happens, the show rule performs a query of the source label and gets the last known label up to that position. I guess this step could be optimized if you need better performance when you have a lot of source labels.

1 Like

Thanks for the effort!

Edit: In the outline the figure’s number is still off by 1.

I actually made it work. The trick is to wrap another figure around the styled figure. You just have to decrement the figure-counter by one and remove the wrapper-figure from the outline.

#let fig(
  caption: [], 
  source: [],
  show_source: true,
  fig
) = {
  
  let styled_figure = {
    show figure.caption: it => context {
      it.counter.update(it.counter.get().at(0) - 1)
      if show_source {
        if (it.kind == "i-figured-image" or it.kind == "i-figured-raw") and source == [] {
          it + footnote([Quelle: Eigene Darstellung])
        } else if it.kind == "i-figured-table" and source == [] {
          it + footnote([Quelle: Autor])
        } else {
          it + footnote([Quelle: #source])
        }
      } else {
        it
      }
    } 
  
    figure(caption: caption, fig) //#label
  }

  return figure(outlined: false, styled_figure)
}


Then add a label like normal:

#fig(
  caption: [This is my custom figure],
  source: [@befragung_lf[S. 125]],
  rect(width: 100pt, height: 25pt, [IMAGE])
) <test>

P.S. My code isn’t copy-paste, as I am using the i-figured package.

Do you actually need the show rule? If the only thing you want to do is to attach a footnote to the caption, there is another way.

First try
#let fig(
  caption: [],
  source: [],
  show-source: true,
  ..args,
  fig
) = {
  let kind = args.named().at("kind", default: none)
  let fn = if not show-source {
    none
  } else if source == [] and kind == "i-figured-image" {
    footnote([Quelle: Eigene Darstellung])
  } else {
    footnote([Quelle: #source])
  }
  figure(
    caption: caption + fn,
    ..args,
    fig
 )
}

#fig(
  caption: "A rect with a source",
  source: "The Typst rect function",
  rect()
) <fig:rect>

#fig(
  caption: "A rect without source",
  show-source: false,
  rect()
) <fig:rect2>

@fig:rect and @fig:rect2

For the time being this solution is not complete since you will find another bug, but this time related to the outline. When a footnote is attached to an outlined object the footnote is going to be repeated. In this comment of the related issue in GitHub there is a solution for this kind of problems. Given that, the full solution is this one

#set page(height: auto)

#let in-outline = state("in-outline", false)
#show outline: it => {
  in-outline.update(true)
  it
  in-outline.update(false)
}

#let flex-caption(inside, outside) = context if in-outline.get() { inside } else { outside }

#let fig(
  caption: [],
  source: [],
  show-source: true,
  ..args,
  fig
) = {
  let kind = args.named().at("kind", default: none)
  let fn = if not show-source {
    none
  } else if source == [] and kind == "i-figured-image" {
    footnote([Quelle: Eigene Darstellung])
  } else {
    footnote([Quelle: #source])
  }
  figure(
    caption: flex-caption(caption, caption + fn),
    ..args,
    fig
 )
}

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

#fig(
  caption: "A rect with a source",
  source: "The Typst rect function",
  rect()
) <fig:rect>

#fig(
  caption: "A rect without source",
  show-source: false,
  rect()
) <fig:rect2>

@fig:rect and @fig:rect2
Result

fig-with-source

One last comment: in case you are using the show-source argument just to avoid the footnote when the source is an empty content, I suggest to get rid of that and add another branch in the if statement to avoid the creation of a footnote in that case.

1 Like

Unfortunately this didn’t work with the i-figured package, because i-figured creates yet another copy. That means that your fix for the duplicated footnote didn’t work.

But I found a solution using this function to determine which counter to reset. Seems to be working well.

Here is the updated figure function:

#let fig(
  caption: [], 
  source: [],
  show_source: true,
  fig
) = {  
  let styled_figure = {  
    show figure.caption: it => {
      if show_source {
        if (it.kind == "i-figured-image" or it.kind == "i-figured-raw") and source == [] {
          it + footnote([Quelle: Eigene Darstellung])
        } else if it.kind == "i-figured-table" and source == [] {
          it + footnote([Quelle: Autor])
        } else {
          it + footnote([Quelle: #source])
        }
      } else {
        it
      }
    } 

    let fig_type = get_type(fig)
    if fig_type == TYPE_IMG {
      let c = counter(figure.where(kind: "i-figured-image"))
      context c.update(c.get().at(0) - 1)
    } else if fig_type == TYPE_TBL {
      let c = counter(figure.where(kind: "i-figured-table"))
      context c.update(c.get().at(0) - 1)
    } else if fig_type == TYPE_RAW {
      let c = counter(figure.where(kind: "i-figured-raw"))
      context c.update(c.get().at(0) - 1) 
    }     
       
    figure(caption: caption, fig) 
  }
  
  return figure(styled_figure)
}

It’s good you have a solution for your problem! In regards to my solution

That’s really weird, since my provided code doesn’t touch the same elements than i-figured. I noticed I didn’t handle the figure kind correctly, but aside of that I get the expected result. Putting everything together, including some i-figured configurations I get this:

Code
#import "@preview/i-figured:0.2.4"

#set page(height: auto)

// Custom caption configuration
#let in-outline = state("in-outline", false)
#show outline: it => {
  in-outline.update(true)
  it
  in-outline.update(false)
}

#let flex-caption(inside, outside) = context if in-outline.get() { inside } else { outside }

#let fig(
  caption: [],
  source: [],
  show-source: true,
  ..args,
  fig
) = {
  let kind = args.named().at("kind", default: none)
  if kind == none {
    kind = if (raw, table).contains(fig.func()) { fig.func() } else { image }
  }
  let fn = if not show-source {
    none
  } else if source == [] and (kind == image or kind == raw) {
    footnote([Quelle: Eigene Darstellung])
  } else if source == [] and kind == table {
    footnote([Quelle: Autor])
  } else {
    footnote([Quelle: #source])
  }
  figure(
    caption: flex-caption(caption, caption + fn),
    ..args,
    fig
  )
}

// i-figured configuration
#set heading(numbering: "1.")

#show heading: i-figured.reset-counters.with(extra-kinds: ("atom",))

#show heading: i-figured.reset-counters
#show figure: i-figured.show-figure.with(extra-prefixes: (atom: "atom:"))

// Outlines
#outline()
#i-figured.outline()
#i-figured.outline(target-kind: table, title: "List of Tables")
#i-figured.outline(target-kind: raw, title: "List of Codes")
#i-figured.outline(target-kind: "atom", title: [List of Atoms])


// Document content
= My title

#fig(
  caption: "A rect with a source",
  source: "The Typst rect function",
  rect()
) <rect>

#fig(
  caption: "A code with default source",
  ```typc
  let greetings = "Hello World!"
  ```
) <mycode>

#fig(
  caption: "A table with default source",
  table(columns: 2, [A], [B])
) <mytable>

#fig(
  caption: "Custom kind with source",
  kind: "atom",
  supplement: "Atom",
  source: "The wave equation",
  circle()
) <atom>

#fig(
  caption: "A figure with no source",
  show-source: false,
  circle()
) <circle>


@fig:rect, @lst:mycode, @tbl:mytable, @atom:atom and @fig:circle.
Result

Maybe this doesn’t work due to another show rule or definition in your own code. I still prefer to leave this here in case someone else find it helpful.

1 Like

Thank you! I will try the code this weekend and report back whether it works for me :)

Well, it certainly does not work:

I am not sure if it’s worth the trouble to figure out the problem. I am using a complex template with >600 LOC. A lot might be the cause for this.

Specifically for the outline, you probably need to reconstruct the entry. You can do that from the entry itself. Note that I am not using nested figures as demonstrated above, so probably the it you would get in the show rule would be different.

Code
#let quelle-eigene-darstellung = footnote([Quelle: Eigene Darstellung])
#show outline.entry: it => {
  let f = it.element
  if (
    f.supplement == [Figure] and //
    f.at("kind", default: "") == "i-figured-image" and //
    f.caption.body.has("children")
  ) {
    return [
      #f.supplement
      #numbering(f.numbering, ..f.counter.at(f.location())): // separator ":"
      #f.caption.body.at("children").filter(it => it != quelle-eigene-darstellung).join("")
      #box(width: 1fr, it.fill)
      #it.page
    ]
  }
  return it
}
#let fig(
  caption: [],
  source: [],
  show_source: true,
  lab: "", // str or label
  fig,
) = {
  let caption = caption
  if show_source and source != [] {
    caption += footnote([Quelle: #source])
  }
  if fig.at("kind", default: "") == "i-figured-image" {
    caption = it.caption + quelle-eigene-darstellung
  }
  let body = fig.body

  let b = [
    #figure(
      caption: caption,
      kind: "i-figured-image",
      supplement: "Figure",
      fig.body,
    )
    #if label != "" {
      label(lab)
    }
  ]

  return b
}

#fig(
  caption: "Red",
  source: "Eigene Darstellung",
  figure(rect(fill: red)),
)

#fig(
  caption: "Blue",
  lab: "fig:example-microgrid",
  figure(rect(fill: blue)),
)

// Ref
Die Darstellung eines Microgrids ist in @fig:example-microgrid zu sehen.

#outline(
  title: "Abbildungsverzeichnis",
  target: figure.where(kind: "i-figured-image"),
)
1 Like