What is the type of 'body'?

I am completely new to Typst and am trying to format a heading level that has the following form: Part One: Fundamentals. The plan is to split this string at the colon (see in particular the commented part of the following code) and place the resulting components with different styles on an additional page:

#show heading.where(level: 1): it => {
  pagebreak()
  set align(center + horizon)
  text(
    font: "Frutiger",
    size: 12pt,
    weight: "regular",
    // this is working
    it.body
  )
  v(5em)
  // text(
  //   font: "AvantGarde",
  //   size: 30pt,
  //   // this is not working
  //   it.slice(0, it.body.position(":"))
  // )    
}

Problem is, it.body is no string. This is not surprising, but I have a hard time to find information about the type of this expression. Could someone point me to the documentation part (searching for ‘body’ etc. didn’t return something useful in this context)? Also, exists some conversion functionality for the body ‘object’ returning the string in question from it?

heading.body is of type content, see: Heading Function – Typst Documentation

There is corrently no “official” way to turn content into a string, but a workaround by @Eric can be found here: Turning content into string: `str(content)` · Issue #2196 · typst/typst · GitHub

#let to-string(it) = {
  if type(it) == str {
    it
  } else if type(it) != content {
    str(it)
  } else if it.has("text") {
    it.text
  } else if it.has("children") {
    it.children.map(to-string).join()
  } else if it.has("body") {
    to-string(it.body)
  } else if it == [ ] {
    " "
  }
}

Also when posting code snippeats, make sure to wrap them in triple backticks :slight_smile:

I had found the content type yet, problem was rather its opacity. I will try the function snippet, thank you for it.

I agree with the closed github issue, this kind of (or general) string casting is a candidate for the public interface. Yet my case - creating additional title pages for the parts of a book - is a relatively common pattern.

Combining @aarnent’s suggestion of using the to-string() function created by Eric with your example:

#let to-string(it) = {
  if type(it) == str {
    it
  } else if type(it) != content {
    str(it)
  } else if it.has("text") {
    it.text
  } else if it.has("children") {
    it.children.map(to-string).join()
  } else if it.has("body") {
    to-string(it.body)
  } else if it == [ ] {
    " "
  }
}

#show heading.where(level: 1): it => {
  pagebreak()
  set align(center + horizon)
  text(
    size: 12pt,
    weight: "regular",
    it.body
  )
  v(5em)
  
  let as-string = to-string(it.body)
  let colon-position = as-string.position(":")
  let before-colon = as-string.slice(0, colon-position)
  text(
    size: 30pt,
    before-colon
  )
}

= Part One: Functions
= Part Two: Context


As I understand it an official way to convert content to string will never happen since content is intentionally opaque. Here’s a way to achieve a similar result without having to convert to string (or break at :).

#show heading.where(level: 1): set heading(
    numbering: (params) => "Part " + numbering("1", params) + ": "
  )
#show heading.where(level: 1): it => {
  pagebreak()
  set align(center + horizon)
  text(
    size: 12pt,
    weight: "regular",
    it
  )
  v(5em)
  text(size: 30pt, it.body)
}

= Functions
= Context

The biggest (only?) difference is that you get “Part 1” instead of “Part One”.

I remembered the name-it package exists which takes whole numbers and converts them to their English equivalent (1->“one”). This reduces the difference between the “convert to string and split” and “separate numbering from it.body” methods. I guess there would still be differences in references and an outline. If this becomes a problem for you please specify how it should behave.

New code
#import "@preview/name-it:0.1.0": name-it

#let title-case(word) = {
  upper(word.slice(0, 1))
  word.slice(1)
}

#show heading.where(level: 1): set heading(
    numbering: (params) => "Part " + title-case(name-it(params)) + ": "
  )
#show heading.where(level: 1): it => {
  pagebreak()
  set align(center + horizon)
  text(
    size: 12pt,
    weight: "regular",
    it
  )
  v(5em)
  text(size: 30pt, it.body)
}

= Functions
= Context

Note that the titleize package could also be used for converting oneOne. It would actually be the better choice if you have more than 20 level one headings since my small function would not be correct for anything with two words like “twenty one”.

1 Like

It is a bit more complicated, but the idea of utilizing the numbering might be helpful. My case is the automatic translation of a PDF in English into German. The book was read by a combination of LLM’s and classic python code (pymupdf), the translated content returned as JSON. This way, “Part One: Fundamentals” becomes something like “Teil Eins: Grundlagen”.

The complete typst code will be generated from said Python project and a resulting PDF will be created from it. The part string will serve as a level 1 header and is re-used in different places, the mentioned title page, where its broken-up components finally have to be at exact positions of background images (semantics and length counts here: 1 or “Eins”) for every part, also as slightly changed header text together with the chapter names on every page for all the chapters of the respective part and also at some other places.

Not quite sure yet about my final approach, I’m still testing reasonable general approachs of transforming the header. For this, type-safe access to all his building blocks - strings one of them - would be desirable.

Is it important that the heading (“Part One: Fundamentals”) be the same element as the title page (“Fundamentals”)? If not, you could define your own function that places the heading, then places the title page:

#import "@preview/name-it:0.1.0": name-it

#let title-case(word) = {
  upper(word.slice(0, 1))
  word.slice(1)
}

#let make-title(body) = {
  set align(center + horizon)
  set text(size: 30pt)
  body
}

#let heading-and-title(body) = {
  pagebreak(weak: true)
  set align(center)
  set text(size: 12pt, weight: "regular")
  heading(body, numbering: (params) => "Part "  + title-case(name-it(params)) + ": ")
  make-title(body)
}

#outline()
#heading-and-title[Fundamentals]
#lorem(50)
== More details
#lorem(50)
#heading-and-title[Other]
#lorem(50)

This way when other parts of your document go looking for level one headings it only finds what was created by the call to heading(...) in heading-and-title().

As for counting in German, I don’t know of any Typst Universe package that does that. If you can generate the “Eins”, “Zwei”, etc… in your Python script, then you could modify heading-and-title() to accept another parameter then insert it into the numbering:

#let heading-and-title(part-num, body) = {
  pagebreak(weak: true)
  set align(center)
  set text(size: 12pt, weight: "regular")
  heading(body, numbering: (params) => "Part " + part-num + ":")
  make-title(body)
}
//Call it like this:
#heading-and-title[Zwei][Würst]

As others have said, the type is content; how can you find that (and more) out?

Let’s take this simple example:

#show heading: it => {
  it.body
}

= Example

Your first tools are repr(), type() and panic(); the last is sometimes useful when it’s not trivial to put something into the document:

#show heading: it => {
  repr(it.body)  // [Example]
  [#type(it.body)]  // content
  // panic(it.body)  // Panicked with: [Example]
}

… that’s something, but for content, type() is not enough. The next level are func() and fields():

#show heading: it => {
  [#it.body.func()]  // text
  [#it.body.fields()]  // (text: "Example")
}

You see that the heading body (in this case) is text, with the single field text (all the other fields are not actually part of the text element; that’s a specialty of the text element).

With that knowledge, you can inspect content, and construct snippets such as the “turning content into string” one.

4 Likes

After two days, I’m in no way close to have a grasp for even the syntax yet :) I tried type, but as type(it.body), not [#type(it.body)]. This returned an error, and I continued elsewhere. Mind me.

The pointer to the introspection methods is very appreciated! Guilty by ignorance, I have very likely to absorb more of the fundamentals as the next step. But so far, I like the technology and the results.

1 Like

t4t – Typst Universe has get.text.

1 Like

This is a situation where panic(type(...)) is also useful, because you can always panic.

The problem is that type() returns a value of type type, not content. Let’s say you try to “print” this (more accurately: emit it as content to your document) by putting it in a code block, and that code block contains some other content:

#{
  [abc]
  type("")
}

This means that the two values are “joined”; it’s equivalent to [abc] + type(""). But joining is only supported for certain (combinations of) types: content, content and str, arrays, … but not content and type!

Therefore you need to put this into a content block: [abc] + [#type("")]. For repr() this wasn’t necessary, since it returns a str, which can be joined with content without issue.

No. Because of context behavior, panicing in context doesn’t always work. I faced this issue many times, and it’s really annoying, since panic is basically the only tool Typst provide for debugging anything.

1 Like