/* * 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 // 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(items) = block[ #let c = counter("exercise_items") #c.update(0) #table( columns: (1fr, auto), stroke: none, ..for (points, statement) in items { let pt if points <= 1 { pt = get_L("point_short") } else { pt = get_L("points_short") } ( [#c.step()#v(4pt) *#context c.display("a)")* #statement], rect(radius:1cm, inset: (x:5pt, y:3pt), stroke: gray+0.5pt)[#points #pt], ) } )] // 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_header(title, points) = { let exercise_counter = counter("exercise") context { let exercise_registry = state("exercise-registry").get() exercise_counter.step() let exercise_label = exercise_counter.display() // 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()}*]], [#title], [#rect(radius:1cm, outset: 4pt, inset: (x:2pt, y:0pt), fill: white)[*#format_points(points)*]], ) )} #let exam( title: "%KLAUSUR or EXAM%", subtitle: [%COURSE_TITLE_FIRST_LINE% \ %COURSE_TITLE_SECOND_LINE%], header_left: "%COURSE_SHORT_TITLE%", header_right: "%DATE%", course_number: "%COURSE_NUMBER%", duration_minutes: "%DURATION%", instructions: none, language: "de", // or "en" font-size: 12pt, paper-size: "a4", body ) = { /* * Setup */ // Paper and text size set page(paper: paper-size) set text(size: font-size, lang: language, hyphenate: true) /* * Localization */ let translations = none if language == "en" { translations = ( course_number: "Course number", exam_duration: "Exam duration", student_name: "Name", student_number: "Student number", trainer_name: "Trainer", exercise: "Exercise", point: "point", points: "points", point_short: "pt", points_short: "pts", total_points: "Total", minutes: "minutes", ) } else if language == "de" { translations = ( course_number: "LV-Nummer", exam_duration: "Arbeitszeit", student_name: "Name", student_number: "Matrikelnr.", trainer_name: "Übungsleiter", exercise: "Beispiel", point: "Punkt", points: "Punkte", point_short: "P.", points_short: "P.", total_points: "Gesamt", minutes: "Minuten", ) } state("translations").update(translations) // State and counter for exercises context { let exercise_registry = state("exercise-registry", ()) } // Displays an overview of the exam as a table, with // a row per exercise, the corresponding points and space to write the mark. // The let exercise_overview_table() = context { let entries = state("exercise-registry").final() let total_points = entries.fold(0, (sum, entry) => sum + entry.at(2)) align(center)[ #set text(size: 14pt) #table( columns: (auto, auto), stroke: frame(0.5pt), fill: none, inset: 10pt, align: horizon, table.header[#get_L("exercise")][#get_L("points", cap: true)], table.hline(stroke: 1pt), ..for entry in entries { ( [#entry.at(0)], [#h(2cm) / #entry.at(2)], ) }, table.hline(stroke: 1pt), [*#get_L("total_points")*], [#h(2.2cm) / *#total_points*] ) ] } // Header and footer set page( header: [ #header_left #h(1fr) _#(header_right)_ ], footer: [#align(right)[#context(counter(page).display("1 / 1", both:true))]], ) // Title page v(1fr) align(center)[= #title] v(0.5cm) align(center)[== #subtitle] v(1fr) [*#get_L("course_number")*: #course_number* \ #get_L("exam_duration"): #duration_minutes #get_L("minutes")*] v(0.5cm) table( fill: (lightgray, none), stroke: frame(0.5pt), columns: (auto, 1fr), inset: 10pt, align: horizon, [#get_L("student_name")], [], [#get_L("student_number")], [], [#get_L("trainer_name")], [], // comment this line if unneeded ) v(0.5cm) if (instructions != none) { [ #instructions ] } else { if language == "de" [ - 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. ] if language == "en" [ - 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. ] } v(1fr) // exercise_overview_table() v(1fr) pagebreak() body }