typst / typst

A new markup-based typesetting system that is powerful and easy to learn.
https://typst.app
Apache License 2.0
32.28k stars 864 forks source link

Ability to freeze state #1841

Open edgarogh opened 1 year ago

edgarogh commented 1 year ago

Context / use-case

Slideshow with "fragments" or "pauses"

I would like to create a template for PDF slideshows, with a #fragment[] function that allows me to hide an element at first and reveal it later on a duplicate page (like reveal.js fragments or Beamer's \pauses).

Student quizz / multiple choice question with permuted answers

Described later here (scroll).

Description

To do so, I'd need the ability to duplicate a page, freezing almost all state (including state() variables, heading numbering, page number, etc.), except for one "iteration variable" that tells me which copy I'm laying out currently.

Essentially, I'm looking for a function that would "instantiate" a content at a specific location (regarding state) in a way that resolves/freezes implicit and explicit state inside it, so that any attempt to lay it out multiple times results in the same state materialization.

Possible implementation 1: freeze(content, exclude: list or state)

#let idx = state("idx", 0)
#let body = [= Heading #idx.display() #idx.update(v => v+1)] // depends on implicit heading state

#let body_frozen = freeze(body, exclude: (idx,))

// Headings have the same number
body_frozen
body_frozen
body_frozen

Here, we explicitly exclude a state variable from the freezing, so it can be used and mutated inside.

Note: if a heading is inserted between the call to freeze and the layouts of body_frozen (L5), we would expect the numbering to be in the wrong order, which I guess would be a feature?

Possible implementation 2: clone(content, count: number, exclude: list or state)

This approach would be more "managed" and tailored to my specific case, leading to less room for weird behavior such as unordered numbering:

#let idx = state("idx", 0)
#let body = [= Heading #idx.display() #idx.update(v => v+1)]

clone(body, exclude: (idx,), count: 3) // Creates 3 copies of body, with only "idx" being mutated

Another possible approach would be to provide an map-like usage:

#let idx = state("idx", 0)
#let body = [= Heading #idx.display()]

clone(body, list: (0, 1, 2), state: idx)

Here, the list elements would be mapped to the body, passing their values through the idx state variable. The state variable could maybe have its original value stored and restored after the iteration, so that it isn't observable outside?

Soundness

I must admit, I haven't thought that much about the "soundness" of such a feature, and how it would interact with all the weird "meta" Typst features (locate, query, etc.).

Last note

Maybe this idea is a bad solution to the project I have initially (XY problem). Maybe it should be restricted in other ways. Or made even more generic. Maybe my initial problem should just be a Typst builtin.

sitandr commented 1 year ago

Have you looked into polylux package? It seems like a complete solution to your task.

edgarogh commented 1 year ago

I don't know how I didn't find about Polylux when Googling but that's do the job perfectly!


However, it does indeed look like Polylux suffers from the exact problem I'm describing: if you add numbered headings to your slides, their number increases on each sub-slide, when you'd expect the slide to remain the same overall (apart from the overlays).

Here's an example: https://typst.app/project/rxemPv9LmpjiDKVKTESxnb

PgBiel commented 1 year ago

As a workaround, you can likely store the original location in some write-once state and use it to retrieve counters with .at(loc), and, with that, build a custom "heading" (maybe having a real one show up only on the first slide for referencing purposes). Not a very optimal solution, though.

Truth is, it would be weird to have multiple headings with the same number, as references "wouldn't work". But I think some sort of middle ground can be found here (maybe by only pointing to the first one, for example - although that might seem arbitrary).

sitandr commented 1 year ago

I'm afraid there is mo easy solution that can solve that issue via some let or show issues. So for complex things (that can't be solved by using polylux counters for slides and subslides) to make it viable you need to go inside polylux code.

Maybe it is worth trying to make an issue in Polylux repo, so the could decide what is the best fitting missing consistent Typst feature. It is easy to store the state for some given counters, but impossible for all automatically. Maybe we should add some exporting all the states into dicts and back, maybe something entirely else. I don't know what is the best solution there.

ntjess commented 1 year ago

I realize that https://discord.com/channels/1054443721975922748/1138661247940821146 might solve this issue as well -- If content could be exported to an svg string, you can simply recall the frozen state with image.decode(svg-str)

Pasting the question for reference:

Many publishers require you to provide each figure as a separate file along with your text. With figures made in typst, this is difficult to isolate since outer scope show rules will change the behavior if drawn figures are pasted elsewhere. With the merger of https://github.com/typst/typst/pull/1729, would the mechanisms be in place to, say, let svg-str = export-svg(content)?

The cli query system can then pull those svg strings and write them to a file, if they are e.g. saved to a metadata field

edgarogh commented 12 months ago

I'm coming back with another use-case that I randomly stumbled upon: printing student quizzes (MCQ) with randomised answer order.

Example use

Assume that template and question are defined. The following code creates a 30 page PDF containing the answer sheets of a multiple-choice question exam for 30 students. All of them have almost the same content, with the exception of question answers, which are pseudo-randomly permuted to make cheating harder.

#show template.with(student-count: 30)

= Exam

#question(
  "What's the first letter of the alphabet?",
  right: "A",
  wrong: ("B", "C"),
)

To implement template(), you need to repeat the same content 30 times with just the PRNG seed being different. You don't want any title numbering or question numbering cascading to the following sheet. There are currently two solutions:

The system I describe in this issue could solve this problem very trivially.

laurmaedje commented 7 months ago

I have posted some thoughts here: https://laurmaedje.github.io/posts/frozen-state/

memeplex commented 2 weeks ago

I've been reading the post and pondering on it. I would like to suggest an alternative angle to approach the issue. But at best I have a rough understanding of Typst architecture and internals, so what follows may be babbling.

If I understand correctly, the problem can be illustrated like this (Excalidraw permalink):

image

We have defined some content that depends on some state and we instance that content at different places in the document.

To produce its output each instance of the content must look at the sequence of updates of the state in document order, from the place where it's instantiated upwards.

But this turns ambiguous to which instance we are referring during some operations when we just say "content". Worst, there may be no instance at all. We want to fix this. So far so good.

Now a tentative solution has been expressed in terms of "frozen state":

  1. Somehow all instances of this content are actually the same instance (although instances of regular content aren't).
  2. More perplexing, parts of each instance may still change, like those depending on state2 in the diagram.

From this perspective, both are features of the content, and IMO this is where things get hand-wavy and a bit philosophical about different instances that are actually the same but not quite so.

Wouldn't it be more natural to express this at the instantiation point instead of at the definition point?

For example, regular #content instantiations would by default mean something like #content<here>, while you could explicitly say #content<place1> if you wanted content to be be instantiated with the state existing at place1, or #content<place1, (state2,)> if wanted the same but with state2 taken from place here instead.

Please note that this is not about the syntax (I don't care about it) nor about what places ultimately are (they may be simply labels, or perhaps contexts, I don't have enough knowledge to identify what's the closest underlying Typst concept, so I just called them places as well as I might have called them checkpoints).

git001 commented 2 weeks ago

@laurmaedje

I have posted some thoughts here: https://laurmaedje.github.io/posts/frozen-state/

http://laurmaedje.github.io/ => 404 https://laurmaedje.github.io/posts/frozen-state/ => 404

laurmaedje commented 2 weeks ago

@git001 Thanks for the heads-up. I made some changes the blog's build process. That broke things apparently. I'm rolling them back now. (edit: turns out it was because my GitHub student pro membership ran out and the repo was private ...)