fully dynamic counters

This commit is contained in:
Gaspard Jankowiak 2025-11-17 11:26:05 +01:00
commit 4d81aa3e55

150
lib.typ
View file

@ -51,7 +51,8 @@
} }
) )
// Displays a list of sub-questions and the associated points // Displays a list of sub-questions and the associated points.
// Updates the exercise's points automatically.
// items: array of (int, expression), where // items: array of (int, expression), where
// - int is the number of points this item is worth, // - int is the number of points this item is worth,
// - expression is the item to typeset. // - expression is the item to typeset.
@ -63,11 +64,13 @@
//)) //))
#let exercise_items(items) = block[ #let exercise_items(items) = block[
#let c = counter("exercise_items") #let c = counter("exercise_items")
#let exercise_points = 0
#c.update(0) #c.update(0)
#table( #table(
columns: (1fr, auto), columns: (1fr, auto),
stroke: none, stroke: none,
..for (points, statement) in items { ..for (points, statement) in items {
exercise_points += points
let pt let pt
if points <= 1 { if points <= 1 {
pt = get_L("point_short") pt = get_L("point_short")
@ -79,35 +82,54 @@
rect(radius:1cm, inset: (x:5pt, y:3pt), stroke: gray+0.5pt)[#points #pt], rect(radius:1cm, inset: (x:5pt, y:3pt), stroke: gray+0.5pt)[#points #pt],
) )
} }
)] )
#context {
state("exercise-registry").update( arr => {
if (arr.len() == 0) {
arr
} else {
let new_arr = arr.slice(0, arr.len()-1)
new_arr.push((arr.last().at(0), arr.last().at(1), exercise_points))
new_arr
}
})
}
]
// Displays a header for the exercise, ex. number is incremented automatically // Displays a header for the exercise, ex. number is incremented automatically
// Used to compute the overview table (see below) // Used to compute the overview table (see below)
// title: string, title of the exercise // title: string, title of the exercise
// points: int, the number of points the exercise is worth // points: int, the number of points the exercise is worth
#let exercise_header(title, points) = { #let exercise_header(title, points) = {
// state("exercise_points").update(points)
let exercise_counter = counter("exercise") let exercise_counter = counter("exercise")
exercise_counter.step()
context { context {
let exercise_registry = state("exercise-registry").get() let current_exercise = exercise_counter.get().at(0)
exercise_counter.step() let exercise_registry = state("exercise-registry")
let final_points = 0
if (exercise_registry.final().len() >= current_exercise) {
final_points = exercise_registry.final().at(exercise_counter.get().at(0)-1).at(2)
}
let exercise_label = exercise_counter.display() let exercise_label = exercise_counter.display()
// exercise_registry.update(entries => entries + ((exercise_label, title, points),)) exercise_registry.update(entries => entries + ((exercise_label, title, points),))
}
block(radius: 1cm, stroke: none, fill: lightgray, inset: 1pt, block(radius: 1cm, stroke: none, fill: lightgray, inset: 1pt,
table( table(
columns: (auto, 1fr, auto), columns: (auto, 1fr, auto),
align: (left, center, right), align: (left, center, right),
stroke: none, stroke: none,
[#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#get_L("exercise") #context{exercise_counter.display()}*]], [#title], [#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#format_points(points)*]], [#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#get_L("exercise") #context{exercise_counter.display()}*]], [#title], [#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#format_points(final_points)*]],
) )
)} )}}
#let exam( #let exam(
title: "%KLAUSUR or EXAM%", title: "%KLAUSUR or EXAM%",
subtitle: [%COURSE_TITLE_FIRST_LINE% \ %COURSE_TITLE_SECOND_LINE%], course_title: [%COURSE_TITLE_FIRST_LINE% \ %COURSE_TITLE_SECOND_LINE%],
header_left: "%COURSE_SHORT_TITLE%", course_short_title: "%COURSE_SHORT_TITLE%",
header_right: "%DATE%", date: "%DATE%",
institution: smallcaps("NAWI Graz"),
course_number: "%COURSE_NUMBER%", course_number: "%COURSE_NUMBER%",
duration_minutes: "%DURATION%", duration_minutes: "%DURATION%",
@ -142,10 +164,12 @@ if language == "en" {
student_number: "Student number", student_number: "Student number",
trainer_name: "Trainer", trainer_name: "Trainer",
exercise: "Exercise", exercise: "Exercise",
exercise_short: "Ex.",
point: "point", point: "point",
points: "points", points: "points",
point_short: "pt", point_short: "pt",
points_short: "pts", points_short: "pts",
points_obtained: "Points",
total_points: "Total", total_points: "Total",
minutes: "minutes", minutes: "minutes",
) )
@ -157,10 +181,12 @@ if language == "en" {
student_number: "Matrikelnr.", student_number: "Matrikelnr.",
trainer_name: "Übungsleiter", trainer_name: "Übungsleiter",
exercise: "Beispiel", exercise: "Beispiel",
exercise_short: "Bsp.",
point: "Punkt", point: "Punkt",
points: "Punkte", points: "Punkte",
point_short: "P.", point_short: "P.",
points_short: "P.", points_short: "P.",
points_obtained: "Erreichte Punkte",
total_points: "Gesamt", total_points: "Gesamt",
minutes: "Minuten", minutes: "Minuten",
) )
@ -169,42 +195,67 @@ if language == "en" {
state("translations").update(translations) state("translations").update(translations)
// State and counter for exercises // State and counter for exercises
context { let exercise_registry = state("exercise-registry", ()) } state("exercise-registry").update(())
// Displays an overview of the exam as a table, with // Displays an overview of the exam as a table, with
// a row per exercise, the corresponding points and space to write the mark. // a row per exercise, the corresponding points and space to write the mark.
// The
let exercise_overview_table() = context { let exercise_overview_table() = context {
let entries = state("exercise-registry").final() let exercise_registry = state("exercise-registry")
let entries = exercise_registry.final()
let total_points = entries.fold(0, (sum, entry) => sum + entry.at(2)) let total_points = entries.fold(0, (sum, entry) => sum + entry.at(2))
align(center)[
#set text(size: 14pt) let columns = entries.map((i) => 1fr)
#table( columns.push(1fr)
columns: (auto, auto), [== #get_L("points_obtained")]
stroke: frame(0.5pt), set text(size: 14pt)
fill: none, block(radius: 0.3cm, stroke: none, fill: lightgray, inset: (x:0pt, y:4pt),
inset: 10pt, table(
align: horizon, columns: columns,
table.header[#get_L("exercise")][#get_L("points", cap: true)], stroke: none,
table.hline(stroke: 1pt), fill: none,
..for entry in entries { gutter: 0pt,
( inset: (x, y) => {
[#entry.at(0)], let top = 8pt
[#h(2cm) / #entry.at(2)], if y == 0 { top = 2pt }
) if x == 0 {
}, (left:4pt, right:0pt, top: top, bottom: 0pt)
table.hline(stroke: 1pt), } else if x == entries.len() {
[*#get_L("total_points")*], [#h(2.2cm) / *#total_points*] (left:4pt, right:4pt, top: top, bottom: 0pt)
) } else {
] (left:2pt, right:0pt, top: top, bottom: 0pt)
}
},
align: (x, y) => { if (y == 1) { right+horizon } else { center+horizon }},
..for entry in entries {
(
[#get_L("exercise_short", cap: true) #entry.at(0)],
)
},
[*#get_L("total_points")*],
..for entry in entries {
(
rect(radius:0.2cm, fill: white, width: 100%, inset: 8pt, outset: 0pt, stroke:none)[/#entry.at(2)],
)
},
rect(radius:0.2cm, fill: white, width: 100%, inset: 8pt, outset: 0pt, stroke:black)[#strong([/#total_points])],
)
)
} }
// Header and footer // Header and footer
set page( set page(
header-ascent: 20%,
header: [ header: [
#header_left #set text(size: 8pt)
#h(1fr) #table(
_#(header_right)_ stroke: none,
gutter: 4pt,
inset: 0pt,
align: (x, y) => { if x < 2 { left+bottom } else { right }},
columns: (auto, 1fr, auto),
[#institution], [], [#course_short_title \ _#(date)_],
)
], ],
footer: [#align(right)[#context(counter(page).display("1 / 1", both:true))]], footer: [#align(right)[#context(counter(page).display("1 / 1", both:true))]],
) )
@ -214,23 +265,24 @@ set page(
v(1fr) v(1fr)
align(center)[= #title] align(center)[= #title]
v(0.5cm) v(0.5cm)
align(center)[== #subtitle] align(center)[== #course_title]
v(1fr) v(1fr)
[*#get_L("course_number")*: #course_number* \ #get_L("exam_duration"): #duration_minutes #get_L("minutes")*] [*#get_L("course_number")*: #course_number* \ #get_L("exam_duration"): #duration_minutes #get_L("minutes")*]
v(0.5cm) v(0.5cm)
table( block(radius: 0.2cm, fill: lightgray, inset: 3pt, clip: true, table(
fill: (lightgray, none), fill: lightgray,
stroke: frame(0.5pt), stroke: none,
columns: (auto, 1fr), columns: (auto, 1fr),
inset: 10pt, gutter: 3pt,
inset: (left:7pt, right:0pt, y:0pt),
align: horizon, align: horizon,
[#get_L("student_name")], [], [#get_L("student_name")], rect(fill: white, width: 100%, radius: 0.2cm),
[#get_L("student_number")], [], [#get_L("student_number")], rect(fill: white, width: 100%, radius: 0.2cm),
[#get_L("trainer_name")], [], // comment this line if unneeded [#get_L("trainer_name")], rect(fill: white, width: 100%, radius: 0.2cm),
) ))
v(0.5cm) v(0.5cm)
@ -238,21 +290,25 @@ if (instructions != none) {
[ #instructions ] [ #instructions ]
} else { } else {
if language == "de" [ if language == "de" [
- _Lesen Sie die ganze Prüfungsaufgabe durch_.
- Es sind keine Unterlagen und auch keine Taschenrechner erlaubt. - Es sind keine Unterlagen und auch keine Taschenrechner erlaubt.
- Alle Rechenschritte (inklusive Zwischenresultate und Lösungswege) sind anzugeben und alle Antworten genau zu begründen bzw. zu beweisen! - Alle Rechenschritte (inklusive Zwischenresultate und Lösungswege) sind anzugeben und alle Antworten genau zu begründen bzw. zu beweisen!
- Schreiben Sie auf jedes _lose_ Blatt Ihren Namen und Ihre Matrikelnummer! - Schreiben Sie auf jedes _lose_ Blatt Ihren Namen und Ihre Matrikelnummer!
- Nicht mit roter Farbe schreiben. - Nicht mit roter Farbe schreiben.
- Viel Erfolg!
] ]
if language == "en" [ if language == "en" [
- _Before starting, read the whole exam._
- Course material and calculators are not allowed. - Course material and calculators are not allowed.
- Write down all the steps, including intermediate results. All answers must be substantiated! - Write down all the steps, including intermediate results. All answers must be substantiated!
- Do not write in red. - Do not write in red.
- Good luck!
] ]
} }
v(1fr) v(1fr)
// exercise_overview_table() exercise_overview_table()
v(1fr) v(1fr)