Is there an equivalent of global variable ? Modifying a variable outside current scope and context

Hello,
For a personal project where i create a template for a student songbook i want to maintain a global dictionary where at each song i “instantiate” i insert a new pair (title, page_number). For example :

#import utils:*

#let song(title) = context {
 let pagenum = ...
utils.my_dict_index.insert(title, page_num)
}

I see two problems here :

  • Modifying a variable outside of current function scope
  • Modifying something outside of context.

I’m not sure how to implement this (not even sure if it is possible). My first think goes to #state() function but i don’t really understand the whole principle of it.
So is there a way to do this ?
Thanks in advance for helping.

Hi Alex,

#state is definitely the right approach.
It took me a little while to understand it too, to encourage you.

Can you explain your workflow in more detail?
What exactly should be saved and when and how should it be called?
What is ‘page_number’? The page on which the current song is saved?

If you explain this to me, I can try to help you with the implementation.

Kind regards
Alex (that’s my name too :D)

1 Like

Here’s a link to my project for more insight :
https://typst.app/project/rxJdkxQZb6FrIuv59NVJxS

So basically my project revolve around 3 principals files :

  • style.typ : define the style of each element that constitute a ‘song’
  • format.typ : format a song based on parameters received, using style defined previoulsy. Here i define a context function called “chant” (song in french) that i can use throughout the project (files found in "chant/*.typ).
  • utils.typ : define a few function useful for the whole project

Basically i want to create and empty dictionary in utils.typ that contain all page numbers and title of each song to later construct an index table. The page number is the number of the page on which the song is located (already defined in chant() ).
So i am thinking to update this utils.index_dictionnary at the end of chant() function and use it later in the project

Ahh I see,

then you should use the #outline function.

I added the following code to your #chant command:

#let chant(
  title,
...
) = context{
  hide(heading(title))

Then you can create a outline with:

#outline()

which results in the following result:

1 Like

I’m trying to implement the solution like this :

//in utils.typ
#let index_dict = state("index_dict", (:))

// in format.typ

#let chant(...) = context{
  let dict = utils.index_dict.get()
  dict.insert(title,utils.get_page_number())
  utils.index_dict.update(dict)
}

I get a warning :
Layout did not converge within 5 attempts (warning)

Since my problem can be extrapolated to others problem is it possible to explain why ?

It worked, but now i get a bunch of white pages appearing. I supposed it is due to the height of the heading took into account even though it is set to hidden. I will search to prevent this.

edit : set the size of heading to 0pt

show heading: text.with(size: 0pt)
hide(heading(title))

That said, is there a solution involving #state()? I think my problem could be generalized to something like: ‘How to use or modify a global variable effectively?’

This project seems very complex and large, making it difficult to quickly identify why the layout doesn’t converge. There could be many reasons for this, especially in a project of this scale.

However, here’s a Minimal Working Example (MWE) that uses a global state to store songs and their page numbers, which might help you troubleshoot your issue:

https://typst.app/project/rNyGAAnyyagyfz1y2e3jdU

This is the minimal chants.typ for storing and getting chant title and page number:

// chants.typ

// State for the songs
#let _state_chants = state("chants", ())
#let _page = counter(page)

// Main function to add a song
#let chant(title: "") = {
  // Update state with new song
  context {
    let new_page = _page.get().first()
    _state_chants.update(songs => {
      songs.push((
        title: title,
        page: new_page,
      ))
      songs
    })
  }
  // Show the title as heading
  heading(level: 2, title)
}

// Function to create table of contents
#let chant-outline() = {
  heading(level: 1, "Liedverzeichnis")

  context {
    let songs = _state_chants.final()

    for song in songs [
      #song.title
      #box(width: 1fr, repeat[.])
      #song.page
      #linebreak()
    ]
  }
}

this is an example usage of this file:

//example.typ
#import "chants.typ": *

#set page(
  "a6",
  numbering: "1",
)

// Outline of the songs
#chant-outline()

#pagebreak()

#chant(title: "Amazing Grace")
Lorem ipsum dolor sit amet...

#pagebreak()

#chant(title: "Hallelujah")
Consectetur adipiscing elit...

1 Like

In this case the cause is actually fairly clear: you use get() followed by update() instead of update with a callback. Here is a post where I explain in detail what the problem and fix for it is: Why is State Final not "final"? - #2 by SillyFreak

a bit “cleaner” is I think using place(hide(...)) which prevents the header from being considered for layout at all.

But the state based solution Mathemensch shows (using update(songs => ...) looks good :) I don’t think it has convergence problems.The update() is inside a context and depends on a get() but I think it’s fine because it’s a different counter.

1 Like