| Crates.io | mdbook-exercises |
| lib.rs | mdbook-exercises |
| version | 0.1.5 |
| created_at | 2025-12-27 04:38:21.607428+00 |
| updated_at | 2026-01-06 03:06:24.374861+00 |
| description | An mdBook preprocessor for interactive exercises with hints, solutions, and test execution |
| homepage | https://github.com/guyernest/mdbook-exercises |
| repository | https://github.com/guyernest/mdbook-exercises |
| max_upload_size | |
| id | 2006689 |
| size | 946,040 |
A preprocessor for mdBook that adds interactive exercise blocks with hints, solutions, and optional Rust Playground integration for testing.
cargo install mdbook-exercises
git clone https://github.com/guyernest/mdbook-exercises
cd mdbook-exercises
cargo install --path .
cargo install --git https://github.com/guyernest/mdbook-exercises
[preprocessor.exercises]
Copy the assets to your book's theme directory:
mkdir -p src/theme
cp /path/to/mdbook-exercises/assets/exercises.css src/theme/
cp /path/to/mdbook-exercises/assets/exercises.js src/theme/
Then add to your book.toml:
[output.html]
additional-css = ["theme/exercises.css"]
additional-js = ["theme/exercises.js"]
# Exercise: Hello World
::: exercise
id: hello-world
difficulty: beginner
time: 10 minutes
:::
Write a function that returns a greeting.
::: starter file="src/lib.rs"
```rust
/// Returns a greeting for the given name
pub fn greet(name: &str) -> String {
// TODO: Return "Hello, {name}!"
todo!()
}
```
:::
::: hint level=1
Use the `format!` macro to create a formatted string.
:::
::: hint level=2
```rust
format!("Hello, {}!", name)
```
:::
::: solution
```rust
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
```
:::
::: tests mode=playground
```rust
#[test]
fn test_greet() {
assert_eq!(greet("World"), "Hello, World!");
}
#[test]
fn test_greet_name() {
assert_eq!(greet("Alice"), "Hello, Alice!");
}
```
:::
mdbook build
examples/ folder:
hello-world.md — Beginner Rust exercise with hints, solution, and Playground testscalculator.md — Intermediate Rust with local testsmultilang-python.md — Python exercise (mode=local)multilang-js.md — JavaScript exercise (mode=local)solution-reveal.md — Demonstrates reveal=alwaysch02-environment-setup.md — Setup exercise (ID suffix -setup, position 00){{#exercise ...}}): see sample-book/Defines metadata for the exercise:
::: exercise
id: unique-exercise-id
difficulty: beginner | intermediate | advanced
time: 20 minutes
prerequisites:
- exercise-id-1
- exercise-id-2
:::
Learning outcomes in two categories:
::: objectives
thinking:
- Understand concept X
- Recognize pattern Y
doing:
- Implement function Z
- Write tests for edge cases
:::
Pre-exercise reflection prompts:
::: discussion
- Why might we want to do X?
- What are the tradeoffs of approach Y?
:::
Editable code for the student to complete:
::: starter file="src/main.rs" language=rust
```rust
fn main() {
// TODO: Your code here
}
```
:::
Attributes:
file - Suggested filename (displayed in header)language - Syntax highlighting language (default: rust)Code fence info:
filename or file for suggested filename.Examples:
::: starter
```rust,filename=src/lib.rs
pub fn run() {}
```
:::
::: starter file="src/main.rs"
```rust
fn main() {}
```
:::
Precedence:
file="src/main.rs" overrides filename=... in the fence.Progressive hints with levels:
::: hint level=1 title="Getting Started"
First, consider...
:::
::: hint level=2
Here's more detail...
:::
::: hint level=3 title="Almost There"
```rust
// Nearly complete solution
```
:::
Attributes:
level - Hint number (1, 2, 3, etc.)title - Optional title for the hintThe complete solution, hidden by default:
::: solution
```rust
fn solution() {
// Complete implementation
}
```
### Explanation
Why this solution works...
:::
Attributes:
reveal — on-demand | always | never (controls visibility)Rendering:
reveal attribute controls visibility:
on-demand: Hidden by default unless globally configured to revealalways: Shown expanded regardless of global confignever: Kept hidden; the UI hides the toggleTest code that can optionally run in the browser:
::: tests mode=playground
```rust
#[test]
fn test_example() {
assert!(true);
}
```
:::
Attributes:
mode - Either playground (run in browser) or local (display only)language - Programming language (defaults to the fence language, if present)When mode=playground:
Code fence info:
```rust) sets language if the directive doesn’t specify it.language=... takes precedence over the fence.Implementation details:
lib), combining starter code with test code.Post-exercise questions:
::: reflection
- What did you learn from this exercise?
- How would you extend this solution?
:::
When tests have mode=playground, the preprocessor generates JavaScript that:
Limitations:
std library (no external crates)For exercises requiring external crates, use mode=local and guide users to run cargo test locally.
Exercise completion is tracked in localStorage:
[preprocessor.exercises]
# Enable/disable the preprocessor
enabled = true
# Show all hints by default (useful for instructor view)
reveal_hints = false
# Show solutions by default
reveal_solutions = false
# Enable playground integration
playground = true
# Custom playground URL (for private instances)
playground_url = "https://play.rust-lang.org"
# Enable progress tracking
progress_tracking = true
# Automatically copy CSS/JS assets to your book's theme directory
manage_assets = false
mdbook-exercises can be used as a library for parsing exercise markdown:
use mdbook_exercises::{parse_exercise, Exercise};
let markdown = std::fs::read_to_string("exercise.md")?;
let exercise = parse_exercise(&markdown)?;
println!("Exercise: {}", exercise.metadata.id);
println!("Difficulty: {:?}", exercise.metadata.difficulty);
println!("Hints: {}", exercise.hints.len());
[dependencies]
# Parser only (no rendering, no mdBook dependency)
mdbook-exercises = { version = "0.1", default-features = false }
# With HTML rendering (no mdBook dependency)
mdbook-exercises = { version = "0.1", default-features = false, features = ["render"] }
# Full mdBook preprocessor (default)
mdbook-exercises = { version = "0.1" }
For AI-assisted learning experiences, exercise files can be paired with .ai.toml files containing AI-specific instructions. The parser extracts structured data that MCP servers can use:
use mdbook_exercises::{parse_exercise, Exercise};
// In your MCP server
let exercise = parse_exercise(&markdown)?;
// Access structured data for AI guidance
let starter_code = &exercise.starter.as_ref().unwrap().code;
let hints: Vec<&str> = exercise.hints.iter().map(|h| h.content.as_str()).collect();
let solution = &exercise.solution.as_ref().unwrap().code;
See DESIGN.md for details on MCP integration patterns.
See the examples directory for complete exercise examples:
hello-world.md - Basic exercise structurecalculator.md - Multi-hint exercise with testsmultilang-python.md - Non-Rust example (Python), local testsdouble-exercise.md - Two exercises in one chapter (use include syntax in mdBook for best results)Live Demo: View the rendered examples at guyernest.github.io/mdbook-exercises
docs/exercises-integration.md.-setup, position 00): see docs/setup-exercises.md.See the sample-book directory for a minimal mdBook configured to use mdbook-exercises (and optionally mdbook-quiz). It includes:
book.toml with [preprocessor.exercises] and optional [preprocessor.quiz]src/SUMMARY.md, src/intro.md, and src/exercises.mdsrc/exercises.md demonstrates including exercises via {{#exercise ...}} from this repository’s examples.You can use mdbook-exercises alongside mdbook-quiz. A common book.toml setup looks like:
[preprocessor.quiz]
# mdbook-quiz configuration here
[preprocessor.exercises]
enabled = true
manage_assets = true # copies exercises.css/js to src/theme/
reveal_hints = false
reveal_solution = false
playground = true
progress_tracking = true
[output.html]
# mdBook will load the installed assets from your theme dir
additional-css = ["theme/exercises.css"]
additional-js = ["theme/exercises.js"]
At build time you will see a startup log message similar to mdbook-quiz:
[INFO] (mdbook-exercises): Running the mdbook-exercises preprocessor (vX.Y.Z)
file="..." beats filename=...).language attribute is omitted.language=... overrides fence language. Fence language sets default when omitted.mode is taken from directive attributes.reveal on the solution overrides global config (reveal_solution).Contributions are welcome! Please open an issue or submit a pull request at GitHub.
MIT OR Apache-2.0