Hi.
Depends on your structure. You either pass data from section
to question
and sum the scores, or work with metadata
, context
, and query
.
Yes. Same as above, but instead of section
to question
, there will be question
to questions
.
Passing global to local is harder, but still: either through context metadata or by returning function with partially passed arguments.
Definitely not in terms of implementation, especially if the simplest-to-implement structure is used.
Here are my questions about the code, there is a lot to discuss. Hopefully this will give some answers on how to improve the documentation.
-
What does this supposed to import?
#import "ANDALUCÍA/andal_banco.typ": *
#import "ASTURIAS/astur_banco.typ": *
-
Is this “institute”?
inst: none,
-
Why uppercase is for text.lang
?
set text(lang: "ES")
-
Why fractions are used for vertical spacing instead of length
?
-
- Why
align
is wrapped in block
?
- What horizontal alignment is supposed to do?
- Why
text.hyphenate
is explicitly added if it’s not enabled by default?
block(width: 100%,
align(horizon+center,text(hyphenate: false, weight: "bold", size: 40pt,
upper(title))
)
)
-
Why not use centered line with line.length
?
line(start: (25%, 0%), end: (75%, 0%))
-
This looks like a hack, use block.spacing
.
v(0.1fr)
-
Why 2 specifically? pagebreak.to
pagebreak()
pagebreak()
-
Why is this here when no headings are used?
outline()
-
- Why
scoring
exists, if total score is supposed to be calculated automatically?
- Why you need underscores?
- Why you need 3–4 char long names?
#let question(
scoring: 1,
tag_reg: none,
tag_year: none,
tag_ses: none,
tag_mod: none,
body,
) = {
-
- Why use
block.width
?
- What
box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))
supposed to do?
- Why first 2 parts are explicitly joined through
+
, but last one is joined implicitly?
block(width: 100%, below: 1.6em,
if scoring == 1 {
box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))+[* (#scoring punto)*]
body
} else {
box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))+[* (#scoring puntos)*]
body
}
)
-
Do you really benefit from not writing “o”?
shw: false,
-
- Why use
block
’s stroke
and inset
instead of just rect
?
- Why are you centering a 101% width block? Do you want
block.outset
?
- Why
horizon
is used?
- Why use
text.weight
and not strong
or **
?
- Why use math symbol over
square
?
align(center + horizon, block(width: 101%, stroke: black, inset: (top: 4pt, left: 2.25pt, right: 2.25pt, bottom: 6pt),
align(left+horizon, text(weight: "bold", [Solución: ]) + body + align(right,block(inset: (right:3.75pt),$qed$)))
)
)
-
Why year is not an int
?
tag_year: "2025",
Some other stuff:
- Where are images supposed to be used?
- Why you need a filter, if you filter questions into different files?
- If section only have one criterion and answer, why make separate functions instead of additional section parameters?
- Why add name to
banco.typ
, if it’s already inside a named directory?
Here is a solution, that can be further significantly simplified, if you would mostly work with functions & data, and not content
:
exam_year.typ
#import "preamble.typ": *
#show: title-book.with(
title: "Ejercicios de exámenes",
course: "Todas las comunidades",
subject: "Por años",
inst: none,
show-answers: true,
show-criteria: true,
// filter: (year: 2024)
)
#include "ANDALUCÍA/andal_banco.typ"
// #include "ASTURIAS/astur_banco.typ"
preamble.typ
#let title-book(
title: none,
course: none,
subject: none,
inst: none,
show-answers: false,
show-criteria: false,
filter: (:),
body,
) = {
set text(lang: "es")
v(1.5fr)
// block(width: 100%,
// align(horizon+center,text(hyphenate: false, weight: "bold", size: 40pt,
// upper(title))
// )
// )
align(horizon + center, {
text(40pt, strong(upper(title)))
v(1.75fr)
set block(spacing: 1.2em)
text(20pt, strong(course + " - " + subject))
line(length: 50%)
v(0.1fr)
text(12pt, inst)
})
v(1fr)
pagebreak(to: "odd")
// pagebreak()
outline()
[#metadata(show-answers)<show-answers>]
[#metadata(show-criteria)<show-criteria>]
assert(type(filter) == dictionary)
[#metadata(filter)<filter>]
body
}
#let question(
// scoring: 1,
region: none,
year: none,
session: none,
module: none,
description: "Question description",
..sections,
) = context {
let metadata = (
region: region,
year: year,
session: session,
module: module,
)
let filter = query(<filter>).last().value
for (key, value) in filter {
assert(key in metadata)
if value != metadata.at(key) { return }
}
show: block.with(width: 100%, below: 1.6em)
set enum(numbering: "a)")
let (scorings, sections) = array.zip(..sections)
let scoring = scorings.sum()
let points = if scoring == 1 [punto] else [puntos]
// box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))
h(12.6pt)
[*(#scoring #points)*]
description
sections.join()
}
#let section(scoring: 1, body) = {
let points = if scoring == 1 [punto] else [puntos]
let body = {
show: block.with(width: 100%)
box(width: 100%, inset: (left: 16pt))[(#scoring #points) #body]
}
(scoring, body)
}
#let criteria(body) = context if query(<show-criteria>).last().value { body }
#let answer(body) = context {
if not query(<show-answers>).last().value { return }
show: rect.with(
width: 100%,
outset: (x: 2.25pt),
inset: (top: 4pt, bottom: 6pt, rest: 0pt),
)
[*Solución:* #body]
align(right, pad(right: 3.75pt, square(size: 6pt, fill: black)))
}
ANDALUCÍA/andal_banco.typ
#import "../preamble.typ": *
#question(
// scoring: 1,
region: "Andalucía",
year: 2025,
session: "Ordinaria",
module: "Biomoléculas",
description: [
Los glóbulos rojos mantienen un contenido salino interno del 0,9 %, lo que es crucial para su función. Su membrana plasmática, compuesta en un alto porcentaje por fosfolípidos regula el equilibrio osmótico con el plasma sanguíneo. Considerando estos aspectos, razone:
],
section(scoring: 0.5)[
¿Qué ocurriría con estas células si se inyectara a un individuo una solución salina que hiciera que la concentración final de sales en sangre fuese del 2,2 %?
#criteria[
0.25 por cada respuesta correcta.
]
#answer[
Los glóbulos rojos del organismo se encontrarían en un medio hipertónico y saldría agua de ellos (plasmólisis o crenación) con riesgo de muerte celular
]
],
section(scoring: 0.5)[
¿Y si la concentración final de sales en sangre fuese del 0,01%?
#criteria[
0.25 por cada respuesta correcta.
]
#answer[
Los glóbulos rojos se encontrarían en un medio hipotónico y entraría agua en su interior, aumentando su volumen (turgencia) con el riesgo de estallido (citólisis)
]
],
section(scoring: 1)[
Indique la composición de los fosfolípidos y explique por qué su estructura los hace idóneos para formar membranas biológicas.
#criteria[
0.25 por cada respuesta correcta.
]
#answer[
Composición: una molécula de glicerina (glicerol), dos ácidos grasos y un ácido fosfórico unido a un aminoalcohol (alcohol) (0,4 puntos). Por su carácter anfipático las cabezas polares se orientan hacia el agua y las colas apolares hacia el lado opuesto, alejadas del agua, formando bicapas lipídicas (0,6 puntos)
]
],
)
Here is the simplest implementation:
questions_bank.typ
#import "ANDALUCÍA/banco.typ" as andal
// #import "ASTURIAS/banco.typ" as astur
#let questions-bank = (
..andal.questions,
)
exam_year.typ
#import "preamble.typ": questions, title-book
#import "questions_bank.typ": questions-bank
#show: title-book.with(
title: "Ejercicios de exámenes",
course: "Todas las comunidades",
subject: "Por años",
institute: "Institute",
)
#questions(
show-answers: true,
show-criteria: true,
filter: (year: 2025),
..questions-bank,
)
preamble.typ
#let title-book(
title: "Title",
course: "Course",
subject: "Subject",
institute: none,
body,
) = {
set text(lang: "es")
align(horizon + center, {
set block(spacing: 1.2em)
block(text(40pt, strong(upper(title))))
v(17em)
block(text(20pt)[*#course -- #subject*])
line(length: 50%)
if institute != none [#block(text(12pt, institute))]
})
pagebreak(to: "odd")
body
}
/// Create a question.
#let question(
region: none,
year: none,
session: none,
module: none,
description: "Question description",
sections: (),
) = {
let metadata = (
region: region,
year: year,
session: session,
module: module,
)
for (k, v) in metadata { assert(v != none, message: k + " is not set") }
(metadata: metadata, description: description, sections: sections)
}
/// Create a section.
#let section(
scoring: 1,
problem: "Problem description",
criterion: "Criterion",
answer: "Answer",
) = (
scoring: scoring,
problem: problem,
criterion: criterion,
answer: answer,
)
/// Display a criterion.
#let display-criterion(body) = block(body)
/// Display an answer.
#let display-answer(body) = {
show: rect.with(
width: 100%,
outset: (x: 2.25pt),
radius: 3pt,
inset: (top: 4pt, bottom: 6pt, rest: 0pt),
)
set block(spacing: 0pt)
block[*Solución:* #body]
align(right, pad(right: 3.75pt, square(size: 6pt, fill: black)))
}
/// Display a section.
#let display-section(
scoring: 1,
problem: "Problem description",
criterion: "Criterion",
answer: "Answer",
show-answer: false,
show-criterion: false,
) = {
let points = if scoring == 1 [punto] else [puntos]
show: enum.item
[(#scoring #points) #problem]
if show-criterion { display-criterion(criterion) }
if show-answer { display-answer(answer) }
}
/// Display filtered questions.
#let display-question(
show-answers: false,
show-criteria: false,
filter: (:),
question,
) = {
let (sections, description) = question
let max-score = sections.map(s => s.scoring).sum()
let points = if max-score == 1 [punto] else [puntos]
show: enum.item
set block(below: 1.6em)
set enum(numbering: "a)")
[*(#max-score #points)* #description]
for section in sections {
display-section(
show-answer: show-answers,
show-criterion: show-criteria,
..section,
)
}
}
/// Display filtered questions.
#let questions(
show-answers: false,
show-criteria: false,
filter: (:),
..questions,
) = {
assert(type(filter) == dictionary)
questions = questions
.pos()
.filter(q => filter.pairs().all(((k, v)) => q.metadata.at(k) == v))
for question in questions {
display-question(
show-answers: show-answers,
show-criteria: show-criteria,
question,
)
}
}
ANDALUCÍA/banco.typ
#import "../preamble.typ": *
#let questions = (
question(
region: "Andalucía",
year: 2025,
session: "Ordinaria",
module: "Biomoléculas",
description: [
Los glóbulos rojos mantienen un contenido salino interno del 0,9 %, lo que
es crucial para su función. Su membrana plasmática, compuesta en un alto
porcentaje por fosfolípidos regula el equilibrio osmótico con el plasma
sanguíneo. Considerando estos aspectos, razone:
],
sections: (
section(
scoring: 0.5,
problem: "¿Qué ocurriría con estas células si se inyectara a un individuo una solución salina que hiciera que la concentración final de sales en sangre fuese del 2,2 %?",
criterion: "0.25 por cada respuesta correcta.",
answer: "Los glóbulos rojos del organismo se encontrarían en un medio hipertónico y saldría agua de ellos (plasmólisis o crenación) con riesgo de muerte celular",
),
section(
scoring: 0.5,
problem: "¿Y si la concentración final de sales en sangre fuese del 0,01%?",
criterion: "0.25 por cada respuesta correcta.",
answer: "Los glóbulos rojos se encontrarían en un medio hipotónico y entraría agua en su interior, aumentando su volumen (turgencia) con el riesgo de estallido (citólisis)",
),
section(
scoring: 1,
problem: "Indique la composición de los fosfolípidos y explique por qué su estructura los hace idóneos para formar membranas biológicas.",
criterion: "0.25 por cada respuesta correcta.",
answer: "Composición: una molécula de glicerina (glicerol), dos ácidos grasos y un ácido fosfórico unido a un aminoalcohol (alcohol) (0,4 puntos). Por su carácter anfipático las cabezas polares se orientan hacia el agua y las colas apolares hacia el lado opuesto, alejadas del agua, formando bicapas lipídicas (0,6 puntos)",
),
),
),
)
No context is used, and filtering is only done once, so it’s as performant as it gets. The rest is just styling of preprocessed data.
You can skip the structure functions and just use raw dictionaries, but it might introduce some other issues.