180 lines
4.8 KiB
Plaintext
180 lines
4.8 KiB
Plaintext
#let pctr = counter("points")
|
|
|
|
#let french_month = ("janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre")
|
|
|
|
#let compile_mode = sys.inputs.at("mode", default: "");
|
|
|
|
#let enable_solutions = { compile_mode in ("solution", "notes") };
|
|
#let enable_notes = { compile_mode == "notes" };
|
|
|
|
|
|
|
|
#let points_counter(h) = {
|
|
// Find the first heading at a level equal or lower (more important) than this one
|
|
let headings_after = query(selector(heading)
|
|
.after(h.location(), inclusive: false)
|
|
).filter(nh => nh.level <= h.level);
|
|
|
|
let start_value = pctr.at(h.location())
|
|
let stop_value = if headings_after.len() == 0 { pctr.final() }
|
|
else { pctr.at(headings_after.first().location()) }
|
|
stop_value.at(0) - start_value.at(0)
|
|
}
|
|
|
|
#let show_points(pts) = if pts == 0 {
|
|
none
|
|
} else if pts == 1 {
|
|
[(1 pt)]
|
|
} else {
|
|
[(#pts pts)]
|
|
}
|
|
|
|
// Rend les réponses visibles seulement si compilé avec `--input mode=solution`
|
|
#let solution(content) = if enable_solutions { set text(fill: olive); content } else { hide(content) }
|
|
|
|
#let exam(title: "Exam", course: none, date: none, class: none,
|
|
year: none, author: none, subtitle: none, indications: none,
|
|
time: none, allowed: none, pad_page: false) = doc => {
|
|
|
|
let marker(m) = align(center, text( weight: "bold", size: 30pt, m));
|
|
|
|
{ // Page de garde
|
|
set text(font: "Montserrat")
|
|
|
|
let date = if date == none { datetime.today() } else { date };
|
|
let year = if year == none { date.year() } else { year };
|
|
let total = context [#pctr.final().at(0)]
|
|
|
|
|
|
let indications = if indications == none {
|
|
[
|
|
- Ce test doit être réalisé en maximum *#time*.
|
|
|
|
- Aucun document n'est admis, excepté #allowed .
|
|
|
|
- L'utilisation de tout type de matériel électronique est interdite.
|
|
|
|
- Ne pas oublier d'inscrire votre nom et prénom à l'endroit prévu à cet effet.
|
|
|
|
- Merci de respecter un silence absolu jusqu'à ce que le dernier étudiant
|
|
aie rendu sa copie
|
|
]} else { indications };
|
|
|
|
align(center, text(size:26pt, [#course - #year]))
|
|
align(center, text(size:26pt, title))
|
|
align(center, text(size:15pt, fill: luma(80), subtitle))
|
|
v(5%)
|
|
align(center, text(size:14pt, author))
|
|
v(3%)
|
|
align(center, text(size:14pt, date.display("[day] ") + french_month.at(date.month() - 1) + date.display(" [year]")))
|
|
|
|
v(7%)
|
|
align(left, text(size: 20pt, "Nom :"))
|
|
align(left, text(size: 20pt, "Prénom :"))
|
|
v(3%)
|
|
|
|
line(length: 100%, stroke: luma(80))
|
|
v(3%)
|
|
|
|
text(size: 16pt, "Indications :")
|
|
{
|
|
set text(size: 12pt)
|
|
set list(marker: "●", indent: 2em, body-indent: 1em)
|
|
indications
|
|
}
|
|
|
|
v(5%)
|
|
|
|
align(right, text(size: 12pt, [Total: #total points]))
|
|
|
|
} // Fin page de garde
|
|
|
|
let guard = {
|
|
pagebreak()
|
|
block(width: 100%, height: 100%, align(horizon, marker("PAGE VIDE")))
|
|
}
|
|
|
|
set text(font: "Carlito", size: 12pt)
|
|
|
|
show heading: h => {
|
|
let body = context { counter(heading).display() } + " " + h.body + " " + show_points(points_counter(h));
|
|
pad(bottom: 0.5em, text(fill: eastern, size: 16pt, weight: "light", body))
|
|
|
|
}
|
|
|
|
let head = text(fill: gray)[#course #year - #title];
|
|
|
|
set page(header: head, footer: align(right, text(fill: gray, "Page ") + context counter(page).display("1 | 1", both: true)))
|
|
|
|
if pad_page {
|
|
guard
|
|
}
|
|
|
|
pagebreak()
|
|
|
|
doc
|
|
v(2em)
|
|
marker("FIN")
|
|
|
|
guard
|
|
};
|
|
|
|
// Bloc réponse caché en mode production
|
|
#let reponse(a) = {
|
|
let answ_block = block(width: 100%, inset: 1em, solution(a));
|
|
block(stroke: black, answ_block)
|
|
v(2em, weak: true)
|
|
}
|
|
|
|
#let notes(n) = if enable_notes {
|
|
block(fill: luma(230), radius: 1em, inset: 1em, text(fill: red, n))
|
|
v(2em, weak: true)
|
|
} else {
|
|
none
|
|
};
|
|
|
|
// Question d'examen. Numérotée automatiquement, nombre de points optionnel
|
|
// (défaut 1), bloc de réponse automatique si 2e argument présent
|
|
#let question(points: 1, q, ..a) = {
|
|
context pctr.update(t => t + points)
|
|
let pts = strong(show_points(points));
|
|
|
|
let qctr = counter("question")
|
|
|
|
context { qctr.step(level: 2) }
|
|
let num = context { qctr.display() };
|
|
//v(2em, weak: true)
|
|
block(breakable: false,
|
|
[#num) #q #pts] + if a.pos().len() > 0 {
|
|
reponse(a.pos().at(0))
|
|
} else { none })
|
|
|
|
}
|
|
|
|
#let qcm(points: 1, question_text, qcm_list) = {
|
|
//let checked_box = "☑";
|
|
//let unchecked_box = "☐";
|
|
|
|
let checked_box = box(stroke: black, outset: 0.2em, "X") + " ";
|
|
let unchecked_box = box(stroke: black, outset: 0.2em, hide("X")) + " ";
|
|
set list(marker: unchecked_box, indent: 2em)
|
|
let qcm_box = if enable_solutions {
|
|
show <yes>: i => {
|
|
show box: b => checked_box;
|
|
set text(fill: olive);
|
|
i
|
|
};
|
|
qcm_list
|
|
} else {
|
|
qcm_list
|
|
};
|
|
|
|
block(breakable: false)[
|
|
#question(points: points, question_text)
|
|
#v(0.5em)
|
|
#qcm_box
|
|
]
|
|
v(2em, weak: true)
|
|
|
|
}
|