This commit is contained in:
Gaspard Jankowiak 2025-11-17 20:47:05 +01:00
commit 921082f61d
3 changed files with 66 additions and 40 deletions

View file

@ -20,7 +20,7 @@ A summary is then printed on the front page.
Writing an exam is now easy: Writing an exam is now easy:
#import "@local/exam:0.1.0": exam, exercise_header, exercise_items, mtext #import "@local/exam:0.1.0": exam, exercise, exercise-items, mtext
#show: exam.with( #show: exam.with(
title: "Exam", title: "Exam",
@ -32,7 +32,7 @@ Writing an exam is now easy:
date: "1. January 1970", date: "1. January 1970",
course_short_title: "ABC & EFG", course_short_title: "ABC & EFG",
course_number: "π", course_code: "π",
duration_minutes: "90", duration_minutes: "90",
ask_trainer_name: false, ask_trainer_name: false,
@ -43,37 +43,59 @@ Writing an exam is now easy:
### Defining exercises and items ### Defining exercises and items
A new exercise can be started using `#exercise_header("title", nb_points)`, for example: A new exercise can be started using `#exercise("title", nb_points)`, for example:
#exercise_header("Relations and their properties", 2) #exercise("Relations and their properties", 2)
Consider the relation $R subset NN^2$, defined as follows: Consider the relation $R subset NN^2$, defined as follows:
$ (x, y) in R #h(0.5cm) <==> #h(0.5cm) x + y #mtext[is odd]. $ $ (x, y) in R #h(0.5cm) <==> #h(0.5cm) x + y #mtext[is odd]. $
Is $R$ reflexive? transitiv? symmetrisch? antisymmetrisch? Is $R$ reflexive? transitive? symmetric? antisymmetric?
On can also defined (sub-)items for the exercise using `#exercise_items(items, override_points:true)`, On can also defined (sub-)items for the exercise using `#exercise-items(override_points:true, numbering:"a)", items)`,
where `items` is an array of `(nb_points, statement)`. By default, the number of points the exercise where `items` is an array of `(nb_points, statement)`. By default, the number of points the exercise
is worth is recomputed as the sum of points for all items. This behaviour can be turned off by is worth is recomputed as the sum of points for all items. This behaviour can be turned off by
setting `override_points: false`. setting `override_points: false`.
#exercise_header("Properties of functions", 3) #exercise("Properties of functions", 3)
Let $f : (0, +infinity) → (0, +infinity)$ with $f (x) = e^(-x)$. Let $f : (0, +infinity) → (0, +infinity)$ with $f (x) = e^(-x)$.
#exercise_items(( #exercise-items((
(1, [Is $f$ injective?]), (1, [Is $f$ injective?]),
(1, [Is $f$ surjective?]), (1, [Is $f$ surjective?]),
(1, [Is $f$ bijective?]), (1, [Is $f$ bijective?]),
)) ))
#exercise("Logical operators", 3.14)
Consider the following truthtable:
#align(center, table(
stroke: frame(0.5pt),
columns: (auto, auto, auto),
align: center,
[$A$], [$B$], [$A or B$],
[1], [1], [1],
[1], [0], [1],
[0], [1], [1],
[0], [0], [1]
))
#exercise-items(override-points: false, numbering: "i)", (
(1, [Is the truthtable correct?]),
(1, [If not, fix it.]),
))
All together, this should output something like: All together, this should output something like:
![screenshot](screenshot-example.png) ![screenshot](screenshot-example.png)
### Utilities ### Utilities
These are used in the example above.
- `mtext(str)` to typeset text within math mode using the default text font. - `mtext(str)` to typeset text within math mode using the default text font.
- `frame(stroke_width)` provides `stroke` for use in a `table`, horizontal lines only, - `frame(stroke_width)` provides `stroke` for use in a `table`, horizontal lines only,
top and bottom lines are bold. top and bottom lines are bold.

68
lib.typ
View file

@ -57,13 +57,13 @@
// - 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.
// example: // example:
// #exercise_items(( // #exercise-items((
// (1, [Untersuchen Sie, ob $f$ injektiv ist.]), // (1, [Untersuchen Sie, ob $f$ injektiv ist.]),
// (1, [Untersuchen Sie, ob $f$ surjektiv ist.]), // (1, [Untersuchen Sie, ob $f$ surjektiv ist.]),
// (1, [Geben Sie im Falle ihrer Existenz die Umkehrfunktion $f^(1)$ an.]) // (1, [Geben Sie im Falle ihrer Existenz die Umkehrfunktion $f^(1)$ an.])
//)) //))
#let exercise_items(items) = block[ #let exercise-items(override-points: true, numbering: "a)", items) = block[
#let c = counter("exercise_items") #let c = counter("exercise-items")
#let exercise_points = 0 #let exercise_points = 0
#c.update(0) #c.update(0)
#table( #table(
@ -78,21 +78,24 @@
pt = get_L("points_short") pt = get_L("points_short")
} }
( (
[#c.step()#v(4pt) *#context c.display("a)")* #statement], [#c.step()#v(4pt) *#context c.display(numbering)* #statement],
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 override-points {
if (arr.len() == 0) { context {
arr state("exercise-registry").update( arr => {
} else { if (arr.len() == 0) {
let new_arr = arr.slice(0, arr.len()-1) arr
new_arr.push((arr.last().at(0), arr.last().at(1), exercise_points)) } else {
new_arr let new_arr = arr.slice(0, arr.len()-1)
} new_arr.push((arr.last().at(0), arr.last().at(1), exercise_points))
}) new_arr
}
})
}
} }
] ]
@ -100,7 +103,7 @@
// 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(title, points, numbering: "1") = {
// state("exercise_points").update(points) // state("exercise_points").update(points)
let exercise_counter = counter("exercise") let exercise_counter = counter("exercise")
exercise_counter.step() exercise_counter.step()
@ -111,32 +114,33 @@
if (exercise_registry.final().len() >= current_exercise) { if (exercise_registry.final().len() >= current_exercise) {
final_points = exercise_registry.final().at(exercise_counter.get().at(0)-1).at(2) 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(numbering)
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(final_points)*]], [#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#get_L("exercise") #context{exercise_counter.display(numbering)}*]], [#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%",
course_title: [%COURSE_TITLE_FIRST_LINE% \ %COURSE_TITLE_SECOND_LINE%], course-title: [%COURSE_TITLE_FIRST_LINE% \ %COURSE_TITLE_SECOND_LINE%],
course_short_title: "%COURSE_SHORT_TITLE%", course-short-title: "%course-short-title%",
course-code: "%course-code%",
date: "%DATE%", date: "%DATE%",
institution: smallcaps("NAWI Graz"), institution: smallcaps("NAWI Graz"),
course_number: "%COURSE_NUMBER%", duration-minutes: "%DURATION%",
duration_minutes: "%DURATION%",
ask_student_number: false, ask-student-number: false,
ask_trainer_name: false, ask-trainer-name: false,
ask_group: true, ask-group: true,
instructions: none, instructions: none,
@ -162,7 +166,7 @@ set text(size: font-size, lang: language, hyphenate: true)
let translations = none let translations = none
if language == "en" { if language == "en" {
translations = ( translations = (
course_number: "Course number", course-code: "Course code",
exam_duration: "Exam duration", exam_duration: "Exam duration",
student_name: "Name", student_name: "Name",
student_number: "Student number", student_number: "Student number",
@ -180,7 +184,7 @@ if language == "en" {
) )
} else if language == "de" { } else if language == "de" {
translations = ( translations = (
course_number: "LV-Nummer", course-code: "LV-Nummer",
exam_duration: "Arbeitszeit", exam_duration: "Arbeitszeit",
student_name: "Name", student_name: "Name",
student_number: "Matrikelnr.", student_number: "Matrikelnr.",
@ -260,7 +264,7 @@ set page(
inset: 0pt, inset: 0pt,
align: (x, y) => { if x < 2 { left+bottom } else { right }}, align: (x, y) => { if x < 2 { left+bottom } else { right }},
columns: (auto, 1fr, auto), columns: (auto, 1fr, auto),
[#institution], [], [#course_short_title \ _#(date)_], [#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))]],
@ -271,10 +275,10 @@ set page(
v(1fr) v(1fr)
align(center)[= #title] align(center)[= #title]
v(0.5cm) v(0.5cm)
align(center)[== #course_title] 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-code")*: #course-code \ *#get_L("exam_duration"): #duration-minutes #get_L("minutes")*]
v(0.5cm) v(0.5cm)
@ -286,13 +290,13 @@ block(radius: 0.2cm, fill: lightgray, inset: 3pt, clip: true, table(
inset: (left:7pt, right:0pt, y:0pt), inset: (left:7pt, right:0pt, y:0pt),
align: horizon, align: horizon,
[#get_L("student_name")], rect(fill: white, width: 100%, radius: 0.2cm), [#get_L("student_name")], rect(fill: white, width: 100%, radius: 0.2cm),
.. if ask_student_number { .. if ask-student-number {
([#get_L("student_number")], rect(fill: white, width: 100%, radius: 0.2cm)) ([#get_L("student_number")], rect(fill: white, width: 100%, radius: 0.2cm))
}, },
..if ask_trainer_name { ..if ask-trainer-name {
([#get_L("trainer_name")], rect(fill: white, width: 100%, radius: 0.2cm)) ([#get_L("trainer_name")], rect(fill: white, width: 100%, radius: 0.2cm))
}, },
..if ask_group { ..if ask-group {
([#get_L("group")], rect(fill: white, width: 100%, radius: 0.2cm)) ([#get_L("group")], rect(fill: white, width: 100%, radius: 0.2cm))
} }
)) ))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Before After
Before After