340 lines
8.7 KiB
Typst
340 lines
8.7 KiB
Typst
/*
|
||
* Customisation
|
||
*/
|
||
|
||
#let lightgray = rgb("#eeeeee")
|
||
|
||
// Default font (embedded in Typst)
|
||
#let font-text = "libertinus serif"
|
||
|
||
#let mtext = text.with(font: font-text)
|
||
|
||
/*
|
||
* Macros
|
||
*/
|
||
|
||
// Frame for table, no vertical lines, top and bottom are bold
|
||
// s: stroke, to specify e.g. the width and color of strokes
|
||
#let frame(s) = (x, y) => (
|
||
left: none,
|
||
right: none,
|
||
top: if y > 0 { s } else { 2*s },
|
||
bottom: 2*s,
|
||
)
|
||
|
||
#let capitalize(string) = {
|
||
return string.replace(
|
||
regex("[A-Za-z]+('[A-Za-z]+)?"),
|
||
word => upper(word.text.first()) + lower(word.text.slice(1)),
|
||
)
|
||
}
|
||
|
||
#let get_L(k, cap:false) = context {
|
||
let translations = state("translations").get()
|
||
let term = translations.at(k, default:k)
|
||
if cap {
|
||
return capitalize(term)
|
||
} else {
|
||
return term
|
||
}
|
||
}
|
||
|
||
// Formats number of points, "1 Punkt", "2 Punkte", etc.
|
||
// p: int
|
||
#let format_points = (p) => (
|
||
context {
|
||
if p <= 1 {
|
||
[#p #get_L("point")]
|
||
} else {
|
||
[#p #get_L("points")]
|
||
}
|
||
}
|
||
)
|
||
|
||
// Displays a list of sub-questions and the associated points.
|
||
// Updates the exercise's points automatically.
|
||
// items: array of (int, expression), where
|
||
// - int is the number of points this item is worth,
|
||
// - expression is the item to typeset.
|
||
// example:
|
||
// #exercise-items((
|
||
// (1, [Untersuchen Sie, ob $f$ injektiv ist.]),
|
||
// (1, [Untersuchen Sie, ob $f$ surjektiv ist.]),
|
||
// (1, [Geben Sie im Falle ihrer Existenz die Umkehrfunktion $f^(−1)$ an.])
|
||
//))
|
||
#let exercise-items(override-points: true, numbering: "a)", items) = block[
|
||
#let c = counter("exercise-items")
|
||
#let exercise_points = 0
|
||
#c.update(0)
|
||
#table(
|
||
columns: (1fr, auto),
|
||
stroke: none,
|
||
..for (points, statement) in items {
|
||
exercise_points += points
|
||
let pt
|
||
if points <= 1 {
|
||
pt = get_L("point_short")
|
||
} else {
|
||
pt = get_L("points_short")
|
||
}
|
||
(
|
||
[#c.step()#v(4pt) *#context c.display(numbering)* #statement],
|
||
rect(radius:1cm, inset: (x:5pt, y:3pt), stroke: gray+0.5pt)[#points #pt],
|
||
)
|
||
}
|
||
)
|
||
|
||
#if override-points {
|
||
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
|
||
// Used to compute the overview table (see below)
|
||
// title: string, title of the exercise
|
||
// points: int, the number of points the exercise is worth
|
||
#let exercise(title, points) = {
|
||
// state("exercise_points").update(points)
|
||
let exercise_counter = counter("exercise")
|
||
exercise_counter.step()
|
||
context {
|
||
let current_exercise = exercise_counter.get().at(0)
|
||
let exercise-numbering = state("exercise-numbering").get()
|
||
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(exercise-numbering)
|
||
exercise_registry.update(entries => entries + ((exercise_label, title, points),))
|
||
block(radius: 1cm, stroke: none, fill: lightgray, inset: 1pt,
|
||
table(
|
||
columns: (auto, 1fr, auto),
|
||
align: (left, center, right),
|
||
stroke: none,
|
||
[#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#get_L("exercise") #context{exercise_counter.display(exercise-numbering)}*]], [#title], [#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#format_points(final_points)*]],
|
||
)
|
||
)}}
|
||
|
||
#let exam(
|
||
title: "%KLAUSUR or EXAM%",
|
||
|
||
course-title: [%COURSE_TITLE_FIRST_LINE% \ %COURSE_TITLE_SECOND_LINE%],
|
||
course-short-title: "%course-short-title%",
|
||
course-code: "%course-code%",
|
||
|
||
date: "%DATE%",
|
||
|
||
institution: smallcaps("NAWI Graz"),
|
||
|
||
duration-minutes: "%DURATION%",
|
||
|
||
ask-student-number: false,
|
||
ask-trainer-name: false,
|
||
ask-group: true,
|
||
|
||
instructions: none,
|
||
|
||
language: "de", // or "en"
|
||
font-size: 12pt,
|
||
paper-size: "a4",
|
||
|
||
exercise-numbering: "1",
|
||
|
||
body
|
||
) = {
|
||
|
||
/*
|
||
* Setup
|
||
*/
|
||
|
||
// Paper and text size
|
||
set page(paper: paper-size)
|
||
set text(size: font-size, lang: language, hyphenate: true)
|
||
|
||
state("exercise-numbering").update(exercise-numbering)
|
||
|
||
/*
|
||
* Localization
|
||
*/
|
||
|
||
let translations = none
|
||
if language == "en" {
|
||
translations = (
|
||
course-code: "Course code",
|
||
exam_duration: "Exam duration",
|
||
student_name: "Name",
|
||
student_number: "Student number",
|
||
trainer_name: "Trainer",
|
||
group: "Group",
|
||
exercise: "Exercise",
|
||
exercise_short: "Ex.",
|
||
point: "point",
|
||
points: "points",
|
||
point_short: "pt",
|
||
points_short: "pts",
|
||
points_obtained: "Points",
|
||
total_points: "Total",
|
||
minutes: "minutes",
|
||
)
|
||
} else if language == "de" {
|
||
translations = (
|
||
course-code: "LV-Nummer",
|
||
exam_duration: "Arbeitszeit",
|
||
student_name: "Name",
|
||
student_number: "Matrikelnr.",
|
||
trainer_name: "Übungsleiter",
|
||
group: "Übungsgruppe",
|
||
exercise: "Beispiel",
|
||
exercise_short: "Bsp.",
|
||
point: "Punkt",
|
||
points: "Punkte",
|
||
point_short: "P.",
|
||
points_short: "P.",
|
||
points_obtained: "Erreichte Punkte",
|
||
total_points: "Gesamt",
|
||
minutes: "Minuten",
|
||
)
|
||
}
|
||
|
||
state("translations").update(translations)
|
||
|
||
// State and counter for exercises
|
||
state("exercise-registry").update(())
|
||
|
||
// Displays an overview of the exam as a table, with
|
||
// a row per exercise, the corresponding points and space to write the mark.
|
||
|
||
let exercise_overview_table() = context {
|
||
let exercise_registry = state("exercise-registry")
|
||
let entries = exercise_registry.final()
|
||
let total_points = entries.fold(0, (sum, entry) => sum + entry.at(2))
|
||
|
||
let columns = entries.map((i) => 1fr)
|
||
columns.push(1fr)
|
||
[== #get_L("points_obtained")]
|
||
set text(size: 14pt)
|
||
block(radius: 0.3cm, stroke: none, fill: lightgray, inset: (x:0pt, y:4pt),
|
||
table(
|
||
columns: columns,
|
||
stroke: none,
|
||
fill: none,
|
||
gutter: 0pt,
|
||
inset: (x, y) => {
|
||
let top = 8pt
|
||
if y == 0 { top = 2pt }
|
||
if x == 0 {
|
||
(left:4pt, right:2pt, top: top, bottom: 0pt)
|
||
} else if x == entries.len() {
|
||
(left:2pt, right:4pt, top: top, bottom: 0pt)
|
||
} else {
|
||
(left:2pt, right:2pt, 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
|
||
set page(
|
||
header-ascent: 20%,
|
||
header: [
|
||
#set text(size: 8pt)
|
||
#table(
|
||
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))]],
|
||
)
|
||
|
||
// Title page
|
||
|
||
v(1fr)
|
||
align(center)[= #title]
|
||
v(0.5cm)
|
||
align(center)[== #course-title]
|
||
v(1fr)
|
||
|
||
[*#get_L("course-code")*: #course-code \ *#get_L("exam_duration"): #duration-minutes #get_L("minutes")*]
|
||
|
||
v(0.5cm)
|
||
|
||
block(radius: 0.2cm, fill: lightgray, inset: 3pt, clip: true, table(
|
||
fill: lightgray,
|
||
stroke: none,
|
||
columns: (auto, 1fr),
|
||
gutter: 3pt,
|
||
inset: (left:7pt, right:0pt, y:0pt),
|
||
align: horizon,
|
||
[#get_L("student_name")], rect(fill: white, width: 100%, radius: 0.2cm),
|
||
.. if ask-student-number {
|
||
([#get_L("student_number")], rect(fill: white, width: 100%, radius: 0.2cm))
|
||
},
|
||
..if ask-trainer-name {
|
||
([#get_L("trainer_name")], rect(fill: white, width: 100%, radius: 0.2cm))
|
||
},
|
||
..if ask-group {
|
||
([#get_L("group")], rect(fill: white, width: 100%, radius: 0.2cm))
|
||
}
|
||
))
|
||
|
||
v(0.5cm)
|
||
|
||
if (instructions != none) {
|
||
[ #instructions ]
|
||
} else {
|
||
if language == "de" [
|
||
- _Lesen Sie die ganze Prüfungsaufgabe durch_.
|
||
- 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!
|
||
- Schreiben Sie auf jedes _lose_ Blatt Ihren Namen und Ihre Matrikelnummer!
|
||
- Nicht mit roter Farbe schreiben.
|
||
- Viel Erfolg!
|
||
]
|
||
if language == "en" [
|
||
- _Before starting, read the whole exam._
|
||
- Course material and calculators are not allowed.
|
||
- Write down all the steps, including intermediate results. All answers must be substantiated!
|
||
- Do not write in red.
|
||
- Good luck!
|
||
]
|
||
}
|
||
|
||
v(1fr)
|
||
|
||
exercise_overview_table()
|
||
|
||
v(1fr)
|
||
|
||
pagebreak()
|
||
|
||
body
|
||
}
|