Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/preview/mcx/0.2.1/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright © 2026 <1zumiSagiri>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
98 changes: 98 additions & 0 deletions packages/preview/mcx/0.2.1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# mcx.typ

A Typst package for typesetting randomized multiple-choice exams.

`mcx.typ` is an independent implementation for Typst.
It is inspired by the user-facing functionality of the LaTeX package [_mcexam_](https://ctan.org/pkg/mcexam), and is redesigned specifically for Typst’s typesetting and scripting model.

## Features

- Define multiple-choice questions and answers.
- Generate multiple versions of an exam with randomized question and answer order.
- Options to control shuffling behavior:
- No shuffling.
- Shuffle all questions and answers.
- Shuffle questions while grouping related questions together.
- Shuffle only answers.
- Permute all answers.
- Permute all but the last `n` answers (e.g., "None of the above").
- User-defined permutation order.
- No shuffling.
- Produce answer keys.
- Support for code blocks within questions and answers.

## Quick Start

Below is an example of how to use `mcx` to create a simple multiple-choice exam with two questions, generate two versions of the exam, and produce an answer key with a concept review sheet for permutation verification.

<details>
<summary>Example</summary>
<img src="https://github.com/1zumiSagiri/mcx/blob/v0.2.1/docs/images/quick_start_example.png" alt="Quick Start Example" width="600px" />
</details>

<details>
<summary>Show code</summary>

````typst
#import "@preview/mcx:0.2.1": *

#let qs = (
mc-question(
[
What does this OCaml function do?

```ocaml
let rec fib n =
if n <= 1
then n
else
fib (n - 1) + fib (n - 2)
```
],
(
mc-answer([Calculates the n-th Fibonacci number.], mark: "correct"),
mc-answer([Calculates the factorial of n.]),
mc-answer([Calculates the n-th prime number.]),
mc-answer([Calculates the sum of the first n natural numbers.]),
),
permute: "permuteall",
),
mc-question(
[
Given function: $f(x) = x^3 - 2x^2 + 5x - 7$

Calculate $f'(2)$.
],
(
mc-answer([$9$], mark: "correct"),
mc-answer([$22$]),
mc-answer([$5$]),
mc-answer([$-7$]),
),
permute: (type: "fixlastn", n: 2),
)
)

#mc-questions(qs, output: "exam", number-of-versions: 2, version: 1, seed: 6)

#mc-questions(qs, output: "exam", number-of-versions: 2, version: 2, seed: 6)

#pagebreak()
#mc-questions(qs, output: "key", number-of-versions: 2, seed: 6)

#mc-questions(qs, output: "concept", number-of-versions: 2, seed: 6)
````

</details>

See [`tests/example.typ`](https://github.com/1zumiSagiri/mcx/blob/v0.2.1/tests/example.typ) for a complete example.

## Usage

Import the package using

```typst
#import "@preview/mcx:0.2.1": *
```

The full documentation is available in the [manual](https://github.com/1zumiSagiri/mcx/blob/v0.2.1/docs/manual.pdf).
1 change: 1 addition & 0 deletions packages/preview/mcx/0.2.1/src/lib.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#import "mcx.typ": mc-question, mc-answer, mc-questions, mc-gen-split-script
131 changes: 131 additions & 0 deletions packages/preview/mcx/0.2.1/src/mcx.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// mcx.typ — A Typst package for typesetting multiple-choice exams. It is a Typst implementation inspired by the LaTeX package *mcexam*, redesigned for Typst's capabilities.
//
// Core features implemented:
// - Define multiple-choice questions with answers, correctness/points, explanations, notes.
// - Generate multiple deterministic versions via seed.
// - Randomize question blocks (respecting `follow`) and answer order (permute types).
// - permute types: permuteall, fixlast, fixlastn, ordinal, permutenone, user-defined permutations.
// - Render different outputs: concept, exam, answers, key.
// - Render permutation tables and answer key tables.
//
// Minimum Typst version: 0.14 (required by suiji package).

#import "utils.typ": *

// -------------------------
// Public data constructors
// -------------------------

/// Create an answer.
/// - body (content): content of the answer.
/// - mark (string | number): "correct" or a number (points). Use none for 0.
#let mc-answer(body, mark: none) = (
body: body,
mark: mark,
)

/// Create a question object.
/// - `body` (content): The core content of the question. Supports text, math equations, and code blocks.
/// - `answers` (array): An array of answer objects generated using the `mc-answer` function.
/// - `follow` (boolean): Connectivity logic. If `true`, this question is bundled into a "block" with the previous one, ensuring they are shuffled together as a single unit. Ideal for reading comprehension or data analysis sets.
/// - `instruction` (content): Optional introductory text (e.g., "Read the following passage to answer questions 1-3") that appears *before* the question body.
/// - `explanation` (content): Optional solution or explanation shown only in the `concept` and `answers` output modes.
/// - `notes` (content): Optional internal notes or metadata shown only in the `concept` output mode.
/// - `permute`: Logic for shuffling answer choices.
/// - `"permuteall"` **Full Random**: Shuffles all choices randomly (Default).
/// - `"fixlast"` **Fix Last**: Shuffles all choices except for the very last one (useful for "None of the above").
/// - `(type: "fixlastn", n: 2)` **Fix Last N**: Keeps the specified number of choices at the end of the list static. `n` is clamped to `[1, total answers]`.
/// - `"ordinal"` **Ordinal**: Either no shuffling or reverses the order.
/// - `"permutenone"` **No Shuffling**: Displays choices in the exact order they are defined in the code.
/// - `(1, 3, 2, 4)` **Fixed Map**: Manually forces a specific display order using an array of 1-based indices.
/// - `((1,2..), (2,1..))` **Multi-Version Map**: Provides distinct manual permutations for different versions of the exam.
#let mc-question(
body,
answers,
follow: false,
permute: "permuteall",
instruction: none,
explanation: none,
notes: none,
) = (
body: body,
answers: answers,
follow: follow,
permute: permute,
instruction: instruction,
explanation: explanation,
notes: notes,
)

/// Render multiple-choice questions.
/// Parameters:
/// - `questions`: array of questions created with `mc-question`.
/// - `output`: output mode. One of:
/// - `"concept"`: Concept version with all details.
/// - `"exam"`: Student exam version.
/// - `"answers"`: Answer version with solutions.
/// - `"key"`: Answer key table only.
/// - `number-of-versions`: total number of versions.
/// - `version`: selected version (used when output wants per-version output).
/// - `seed`: positive integer controlling deterministic randomization.
/// - `randomize-questions`: shuffle question blocks.
/// - `randomize-answers`: shuffle answers according to `permute`.
/// - `style`: dictionary allowing users to customize the appearance via predefined or custom styles.
/// - `font` (string): Font family for the output, "Libertinus Serif" by default.
/// - `font-size` (length): Base font size for the output, `11pt` by default.
/// - `v-numbering` (string): Numbering style for versions (e.g., "I", "1", "a"), "I" by default.
/// - `q-numbering` (string): Question numbering style (e.g., "1, 2, 3", "a, b, c", "i, ii, iii"), "1" by default.
/// - `a-numbering` (string): Answer choice numbering style (e.g., "A, B, C", "1, 2, 3", "a, b, c"), "A" by default.
/// - `line-spacing` (length|float|int): Line spacing multiplier, `0.65em` by default.
/// - `margin` (dictionary (string, length)): Page margin size (e.g., top : `1in`).
/// - `config` : boolean dictionary overriding defaults (any of the cfg keys)
/// - `show-per-version`: Show per-version question numbering.
/// - `show-q-perm-table`: Show question permutation table.
/// - `show-q-list`: Show question list.
/// - `show-correct`: Show correct answers.
/// - `show-points`: Show points.
/// - `show-explanation`: Show explanations.
/// - `show-a-perm-table`: Show answer permutation table.
/// - `show-notes`: Show internal notes.
/// - `show-key-table`: Show answer key table.
#let mc-questions(
questions,
output: "exam",
number-of-versions: 4,
version: 1,
seed: 1,
randomize-questions: true,
randomize-answers: true,
config: none,
style: none,
) = {
// Sanitize inputs
let cfg = _cfg(output, config)
let style = _sty(style)
let v = calc.clamp(int(version), 1, number-of-versions)
seed = calc.clamp(int(seed), 1, 2147483647)

// Precompute permutations
let q_order_by_v = _permute_questions(questions, number-of-versions, seed, randomize-questions)
let answers_perm_by_v = _permute_answers(questions, number-of-versions, seed, randomize-answers)

let components = (
_heading_for(output, v, cfg, style),
_question_perm_table(questions, number-of-versions, q_order_by_v, cfg, style),
_question_list(
questions,
if cfg.show-per-version { v } else { 1 },
number-of-versions,
q_order_by_v,
answers_perm_by_v,
cfg,
style,
output,
),
_key_table(questions, number-of-versions, q_order_by_v, answers_perm_by_v, cfg, style),
)

let final_content = components.filter(it => it != none).join()

_apply_style(style, final_content)
}
Loading