I wrote a function that splits the figure caption into a “title” and the “rest” of the caption for my thesis template. Since the caption body can be more complicated than a string, you cannot always directly split the body (text) at the first period. There are probably some cases that are not covered by the function yet, but it should already handle styling with text(), simple functions such as emph(), equations and general content sequences.
// Split a figure caption into a title and the rest
//
// The caption of a figure is usually just a block of content.
// In the context of a thesis the caption can get quite long
// and it would be great if the actual "title" of the figure
// could be inferred from the caption. The title can then be
// highlighted by using a bold font. And the title can also be
// used in the table of figures where using the entire caption
// would just take an insane amount of space.
//
// The approach with this function is to infer the title from
// the caption body based on a specific separator. The easiest
// choice here is to use the full stop/period. A colon could
// also be useful in some cases.
// The separator is not included with either the title or the
// rest since the outline should not show the separator. In
// the regular figure caption, the separator is just appended
// to the title.
//
// An alternative approach is to define a short caption and a
// long caption. The short caption will then be used for the
// outline, and the long caption is used for the figure. This
// is suggested in the Typst Examples Book:
// https://sitandr.github.io/typst-examples-book/book/snippets/chapters/outlines.html#long-and-short-captions-for-the-outline
//
// - body (content): The caption
// - separator (str): The separator to infer the title
// -> (dictionary):
// - title (content)
// - rest (content)
#let split-figure-caption(body, separator) = {
if "text" in body.fields() {
// the actual separation is done here
let position = body.text.position(separator)
return (
title: if position == none { text(body.text) } else { text(body.text.slice(0, position)) },
rest: if position == none { none } else { text(body.text.slice(position + 1)) },
)
} else if "child" in body.fields() {
// this takes care of `styled()` functions such as `text(red)`
let func = body.func()
let args = body.styles
let (title, rest) = split-figure-caption(body.child, separator)
return (title: func(title, args), rest: if rest == none { none } else { func(rest, args) })
} else if "body" in body.fields() {
// this takes care of simple functions such as `emph()`
let func = body.func()
let (title, rest) = split-figure-caption(body.body, separator)
return (title: func(title), rest: if rest == none { none } else { func(rest) })
} else if "children" in body.fields() {
// this takes care of content sequences
let titles = ()
for i in range(body.children.len()) {
let (title, rest) = split-figure-caption(body.children.at(i), separator)
titles.push(title)
if rest != none {
return (
title: titles.join([]),
rest: ((rest,) + body.children.slice(i + 1)).join([]),
)
}
}
}
return (title: body, rest: none)
}
Can you explain why you can’t just change the heading name, or what is the practical purpose of this?
You can simply apply a text show rule to this:
#show outline.entry: it => {
let heading-name = {
show regex("\..*"): "."
it.body()
}
let body = [#heading-name #box(width: 1fr, it.fill) #it.page()]
link(it.element.location(), it.indented(it.prefix(), body))
}
#outline()
= This is figure 1. It does something
This is not how you’re supposed to work in Typst, this should only be used if other more common solutions don’t work. Because regexing text is not the most robust solution.
That might work here with a bit of modification to get the figure captions. The practical purpose is for a thesis in a field where the norm is captions like:
A short title for the figure. Specific details in a few sentences that explain the figure in a lot more text. A bunch more words.
These get printed like:
Figure #. A short title for the figure. Specific details in a few sentences that explain the figure in a lot more text. A bunch more words.
The problem is that then printing to the outline, you get a very long description.
#let in-outline() = {
let after-start = query(selector(<outline-start>).before(here())).len() == 1
let before-end = query(selector(<outline-end>).after(here())).len() == 1
after-start and before-end
}
#let caption(short, details) = context {
if in-outline() { short } else { [#short #details] }
}
#show outline: it => {
[#metadata(none)<outline-start>#it#metadata(none)<outline-end>]
}
#show figure.where(kind: image): set figure(
numbering: (..n) => context {
if in-outline() { numbering("1.", ..n) } else { numbering("1", ..n) }
},
)
#outline(target: figure.where(kind: image), title: [Table of figures])
#figure(
rect(),
caption: caption[A short title for the figure.][Specific details in a few sentences that explain the figure in a lot more text. A bunch more words.],
)
#figure(rect(), caption: caption[A short title for the figure.][#lorem(10)])
This solution is brittle and takes more lines (19 vs. 16):
#let to-string(content) = {
if content.has("text") {
content.text
} else if content.has("children") {
content.children.map(to-string).join("")
} else if content.has("body") {
to-string(content.body)
} else if content == [ ] {
" "
}
}
#show outline.entry: it => link(
it.element.location(),
it.indented(it.prefix(), [
#to-string(it.element.caption.body.children.flatten().join()).split(".").first()
#box(width: 1fr, repeat(".", gap: 0.15em))
#it.page()
])
)
#let in-outline() = {
let after-start = query(selector(<outline-start>).before(here())).len() == 1
let before-end = query(selector(<outline-end>).after(here())).len() == 1
after-start and before-end
}
#let caption(short, details) = context {
if in-outline() { short } else { [#short #details] }
}
#show outline: it => {
[#metadata(none)<outline-start>#it#metadata(none)<outline-end>]
}
#show figure.where(kind: image): set figure(
numbering: (..n) => context {
if in-outline() { numbering("1.", ..n) } else { numbering("1", ..n) }
},
)
It doesn’t work with the caption example you provided:
#outline(target: figure.where(kind: image), title: [Table of figures])
#figure(
rect(),
caption: [A short title for the figure. Specific details in a few sentences that explain the figure in a lot more text. A bunch more words.],
)
Because the content is simple text, so it’s the text function and not sequence. If I add ---, for example, it starts to work.
Also from your example, there should be a period after Figure # in the outline, and your solution doesn’t include it. So it’s not really much simpler, but it also does less stuff and not as robust.