Why does setting `font` inside `show outline.entry` add whitespace to the resulting outline entry?

Consider the code below:

#set heading(numbering: "1.1")

#show outline.entry: it => {  
  set text(font: "TeX Gyre Heros", fill: blue)
  link(
    it.element.location(),
    it.indented(
      it.prefix(),
      it.inner()
    )
  )
}

#outline(
  title: "TOC",
  target: heading
)

= Intro

== #lorem(18)

If I include set text(font: "Some Font") inside the show rule, white space gets prepended to the resulting outline entry:

If I remove font and set it outside the show rule, the issue is resolved.

#set heading(numbering: "1.1")
#set text(font: "Tex Gyre Heros")

#show outline.entry: it => {  
 set text(fill: blue)
 // truncated for readability
}

I ultimately moved the set rule outside the show outline.entry rule, but I’m curious why font specifically seems to be the source of the issue?

The indented function works by measuring the widths of the prefix (i.e. the number) of the outline entries and taking the maximum of them. The width of the number depends on the font used. The indented function can only get the font of the current context, which is provided by the show rule. Since you only set the font inside the show rule, that information is not available to the indented function, unless you put the link in a separate context block as in

#show outline.entry: it => {  
  set text(font: "TeX Gyre Heros", fill: blue)
  context link(
    it.element.location(),
    it.indented(it.prefix(), it.inner())
  )
}

A more idiomatic way would be to use a show-set rule, as then the context provided by the show rule already contains the correct font:

#show outline.entry: set text(font: "TeX Gyre Heros", fill: blue)
#show outline.entry: it => {  
  link(
    it.element.location(),
    it.indented(
      it.prefix(),
      it.inner()
    )
  )
}
4 Likes

When you uncomment the font line it will be the same. I think, the reason is gap.

Got it. Thank you for the clear and concise explanation!

It seems like a bug still… Why doesn’t indented take the context itself? I mean could it not be implemented to do the equivalent of the following?

#show outline.entry: it => {  
  set text(font: "TeX Gyre Heros", fill: blue)
  link(
    it.element.location(),
    context it.indented(it.prefix(), it.inner())
  )
}

IIRC, the intended cannot be implemented in Typst, which can explain that it does not obey all Typst rules. IIUC about measuring, it uses context around the whole show rule, so changing styling inside of it is useless. Although I assume putting set text in indented’s arguments would kinda work.

I don’t think that explains it. Why can’t a built-in function take context? And even assuming that it can’t, then we could simply define

#let indented(..args) = context indented-built-in(..args)

and it would solve the issue I think?

As I said, you can implement it in Typst, but I have to re-read the source code to be sure. If you can, then yeah, you can slap context to it, and it will just work. If that is the case, then there is no point in not having this ability for the built-in one. Although I figure a possible reason is that adding manual context to it for the default show rule would look weird or raise some questions, you don’t normally need context to re-implement default show rules (though not sure how much this is actually true). So yeah, maybe to avoid additional confusion to the already new syntax of utility/building methods on element in show rule.