#let title-state = state("title", "")
#let total_points = counter("points")
/// exam_init: Initialize an exam with a show rule TODO fix me
#let exam_init(body) = {
set page(margin: 40pt)
set text(
font: ("Verdana"),
size: 12pt,
fill: black,
weight: "regular"
)
set raw(theme: "../themes/InspiredGitHub.tmTheme")
show raw: set text(font: "Courier New", weight: "bold", size: 10pt)
set par(spacing: 1.2em)
body
}
/// header: Render a header for the exam, will check total number of points TODO fix me
#let header() = [
#grid(
columns: (1fr, 1fr),
align(center)[
#text(size: 17pt)[
#align(left)[
#"Name:_____________________________"
]
]
],
align(center)[
#text(size: 17pt)[
#align(right)[
#grid(
rows: (0pt, 20pt),
align(center)[
//#context title-state.get()
],
align(right)[
#"____ /" #context total_points.final().at(0) pts
]
)
]
]
]
)
]
#set page(header: [
#context title-state.get()
])
#let setup(title) = {
title-state.update(title)
}
#let spacer() = {
v(10pt)
}
#let cur-question = state(
"num_qs", 1
)
/// question: Create a generic question
/// @param body content Question Body
/// @param points int Number of points the question is worth
#let question(body, points: 1) = context {
cur-question.update(n => n + 1)
total_points.update(p => p + points)
let qnum = cur-question.get()
block(width: 100%, breakable: true, inset: (bottom: 0.5em))[
#grid(
columns: (20pt, 1fr),
column-gutter: 8pt,
text(weight: "bold")[#qnum.],
[#body #h(5pt) (#points pts)]
)
]
}
#let c = counter("letter")
#let answer_indents = (1fr, 10fr, 1fr)
/// _num_to_fr_units: Map a number into a tuple of 1fr units
/// primarily used to make optional column passing to #multiple_choice easier
/// input = 3 -> output = (1fr, 1fr, 1fr)
/// input = 5 -> output = (1fr, 1fr, 1fr, 1fr, 1fr)
/// @param num int number to map
/// @return array Array of num fr units
#let _num_to_fr_units(num) = {
range(num).map(i => 1fr)
}
/// multiple_choice: Create a multiple choice question
/// @param body content Body of question
/// @param points int = 1 Points the question is worth
/// @param cols [int | array ] = 1 Number of columns to render the answer. Pass an array of units for specific spacing e.g. (1fr, 1fr, 12pt)
#let multiple_choice(body, points: 1, cols: 1, ..answers) = {
let cols_type = type(cols)
block[
#c.update(0)
#question(body, points: points)
#v(5pt)
#grid(
columns: answer_indents,
rows: (auto),
"",
block[
#grid(
columns: {
if(cols_type) == int {
_num_to_fr_units(cols)
} else {
cols
}
},
rows: auto,
column-gutter: 5pt,
row-gutter: 15pt,
..answers.pos().map(answer => {
c.step()
block[
#context c.display("a"). #" " #answer
]
})
)
],
"",
)
]
}
/// matching: Create a matching question
/// e.g
/// Cat A. Canine
/// Dog B. Feline
/// Fish C. Aquatic Create
/// @param q_body content body of question to ask
/// @param left_opts array options for the left side of question
/// @param right_opts array options for the right side of question
/// @param points int = 1 points the question is worth
#let matching(q_body, left_opts, right_opts, points: 1) = {
// left and right are shadows
block[
#c.update(0)
#question(q_body, points: points)
#spacer()
#grid(
// should sum to 12, to match answer_indents
// 1st is just a spacer
columns: (1fr, 4fr, 7fr),
"",
align(left)[
#for word in left_opts {
block[
#"____" #word
#spacer()
]
}
],
align(left)[
#for x in right_opts {
block[
#c.step()
#context c.display("a"). #" " #x
#spacer()
]
}
]
)
]
}
// need better name
#let multi_true_false(q_body, ..statements, points: 1) = {
let num = counter("I")
// Note: If you want to skip the first value (N),
// ensure your counter logic matches your document setup.
num.step()
block[
#question(q_body, points: points)
#for statement in statements.pos() {
block[
#grid(
columns: (42pt, 18pt, 9fr, 1fr),
rows: (auto),
"",
block[
#set text(font: "Libertinus Serif")
#context num.display("I").
#context num.step()
],
statement,
"______"
)
]
}
]
}
/// short_answer: Create a short answer question
/// @param q_body content Question Body
/// @param lines int = 1 lines of space to give the user, renders as actual lines
/// @param points int = 1 points the question is worth
#let short_answer(q_body, lines: 1, points: 1) = {
question(q_body, points: points)
// you don't need the full spacing from the question before the first line
v(-10pt)
block(width: 100%, inset: (left: 20pt))[
#for _ in range(lines) {
// line spacing
v(25pt)
line(length: 90%, stroke: 0.5pt)
}
]
}
/// free_response: Create a free response question
/// @param q_body content Question Body
/// @param lines int = 1 lines of space to give the user, renders as empty space
/// @param points int = 1 points the question is worth
#let free_response(q_body, lines: 1, points: 1) = {
question(q_body, points: points)
// i did not know you could just multiply units like that
v(15pt * lines)
}
/// code_block: Create a code block formatted for exams
/// Wraps in box to the edge of the code, can add white space if need it to be longer
/// @param raw_code content(raw) raw code block, eg. ```java public class...```
#let code_block(raw_code) = {
box(stroke: (paint: rgb("d9d9d9"), thickness: 2pt, cap: "round"), inset: (8pt))[
#raw_code
]
}