/* * 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 }