I want to create my own counter with different levels. Sadly I’m struggling with updating the value. Now I just get the error message: “unexpected argument”
#let template(
exercise-heading-prefix: "Exercise",
exercise-heading-numbering-format: "1",
subexercise-heading-prefix: "",
subexercise-heading-numbering-format: "a)",
subsubexercise-heading-prefix: "",
subsubexercise-heading-numbering-format: "(i)",
) = {
// custom exercise headings
let counter-exercise = counter("counter-exercise")
let counter_incr(i) = a => {
// to array
if type(a) == int { a = (a,) }
assert(type(a) == array, message: "counter update got unexpected type")
// a = a.slice(0, i)
// a.at(-1) += 1
while a.len() < 3 {
a.push(0)
}
a.at(i - 1) += 1
for idx in range(i, 3) {
a.at(idx) = 0
}
return a
}
let base_exercise_heading(prefix, num-format, level, content) = {
let w = ("medium", "regular", "regular").at(level - 1)
let fontsize = 16pt - 2pt * (level - 1)
let fup = counter_incr(level)
context counter-exercise.update(fup)
text(size: fontsize, weight: w)[
#prefix
#context {
counter-exercise.display(num-format)
}
#if content != [] [ \- #content ]
]
}
let exercise(prefix: none, num-format: none, content) = {
let level = 1
if prefix == none { prefix = exercise-heading-prefix }
if num-format == none { num-format = exercise-heading-numbering-format }
base_exercise_heading(prefix, num-format, level, content)
}
let subexercise(prefix: none, num-format: none, content) = {
let level = 2
if prefix == none { prefix = subexercise-heading-prefix }
if num-format == none { num-format = subexercise-heading-numbering-format }
base_exercise_heading(prefix, num-format, level, content)
}
return (
"exercise": exercise,
"subexercise": subexercise,
)
}
#let (exercise, subexercise) = template()
#set heading(numbering: "I.")
= Mandatory Exercises
#exercise[]
Here we go. #lorem(30)
#exercise[Binary]
#lorem(50)
#subexercise[]\
#lorem(10)
#subexercise[]\
#lorem(12)
#exercise[Hexa-decimal]
#lorem(30)
= Elective Compulsory Exercises
#exercise[Foo]
#lorem(50). That's all. Trivial.
#exercise(num-format: "I")[Roman]
That was easy.
#exercise[]
#lorem(20)
EDIT: sry missed to send the error, here is a picture
I don’t understand why it’s complaining about an unexpected argument but it is actually pointing to the parameter definition of this anonymous function.
#{
let counter-exercise = counter("counter-exercise")
let counter_incr(i) = (..a) => {
a = a.pos()
while a.len() < 3 { a.push(0) }
a.at(i - 1) += 1
for idx in range(i, 3) { a.at(idx) = 0 }
return a
}
let base_exercise_heading(prefix, num-format, level, content) = {
let w = ("medium", "regular", "regular").at(level - 1)
let fontsize = 16pt - 2pt * (level - 1)
context {
counter-exercise.update(counter_incr(level))
let val = counter-exercise.get().at(level - 1) + 1
text(size: fontsize, weight: w)[
#prefix
#numbering(num-format, val)
#if content != [] [ \- #content ]
]
}
}
}
I’m fine with this for now…
However I still wonder:
Is that really the way to do it? Seems a bit ugly to me. I can well imagine that I have fabricated something here that is actually much simpler.
I believe you can replace counter-exercise.update(counter_incr(level)) by counter-exercise.step(level: level); see docs here: Counter Type – Typst Documentation
Yes, it seems that step with the level parameter behaves like that. Unfortunately it doesn’t state this behavior in the documentation. This is why i thought that complicated about this in the first place. There is definitely room for improvement.
There is still a problem: the step-update of only takes affect right after the surrounding context environment. One possible workaround would be to split the function into two separate context blocks. But is that the supposed design?
You can remove the first context; .step() doesn’t require context. But yes, this is intended. The .step() function simply inserts into the document a command that says “from here onwards in the document, any attempt to read the counter will see its value increased by 1.” Now, what context does is: it creates an element which must be placed into the document. That element is responsible for running the code you wrote inside the context block using the document introspection information made available to it. In particular, when you write here() inside the context block, you will always get the same location: the location of that element. Also, when you get counter values by writing .get(), that is equivalent to .at(here()), so you will always be getting the counter value at the location of that element. Therefore, when you step the counter inside a context block, the updated counter value isn’t visible inside that same context block because you’re always using the location at the start of it. The only way to react to the updated value is to begin the context block after the step.
As a solution to this Question:
The anonymous function I defined is later on called with potentially several integers by counter-exercise.update(). That means that the number of arguments ist variable and it is not an array with variable length. Therefore it is best to use an argument sink like (..a) => {<impl>}.
#let counter_incr(i) = (..a) => {
a = a.pos()
while a.len() < 3 { a.push(0) }
a.at(i - 1) += 1
for idx in range(i, 3) { a.at(idx) = 0 }
return a
}
That is however only the original question.With that being fixed, I discovered (with the help of @PgBiel) that my implementation was overly complicated and the logic I intended with my anonymous functions for different levels is all build in as a sane default in the counter.step() function. (I still think this could be more clear in the documentation). With this knowledge the base_exercise_heading() function can be simplified like this. (This also takes care of some other bugs I didn’t realize before, because I didn’t understand context. Now I do :))