mdbook-exercises 0.1.1

An mdBook preprocessor for interactive exercises with hints, solutions, and test execution
Documentation

mdbook-exercises

A preprocessor for mdBook that adds interactive exercise blocks with hints, solutions, and optional Rust Playground integration for testing.

Features

  • Exercise metadata - Difficulty levels, time estimates, prerequisites
  • Learning objectives - Structured thinking/doing outcomes
  • Discussion prompts - Reflection questions before coding
  • Starter code - Editable code blocks with syntax highlighting
  • Progressive hints - Collapsible, leveled hints that reveal incrementally
  • Solutions - Hidden by default, reveal on demand
  • Test integration - Run tests via Rust Playground or locally
  • Progress tracking - LocalStorage-based completion tracking
  • Accessible - Keyboard navigation, screen reader support

Installation

From crates.io

cargo install mdbook-exercises

From source

git clone https://github.com/guyernest/mdbook-exercises
cd mdbook-exercises
cargo install --path .

From GitHub directly

cargo install --git https://github.com/guyernest/mdbook-exercises

Quick Start

1. Add to your book.toml

[preprocessor.exercises]

2. Include the CSS and JavaScript assets

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"]

3. Create an exercise in Markdown

# 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!");
}
```
:::

4. Build your book

mdbook build

Directive Reference

Exercise Block

Defines metadata for the exercise:

::: exercise
id: unique-exercise-id
difficulty: beginner | intermediate | advanced
time: 20 minutes
prerequisites:
  - exercise-id-1
  - exercise-id-2
:::

Objectives Block

Learning outcomes in two categories:

::: objectives
thinking:
  - Understand concept X
  - Recognize pattern Y

doing:
  - Implement function Z
  - Write tests for edge cases
:::

Discussion Block

Pre-exercise reflection prompts:

::: discussion
- Why might we want to do X?
- What are the tradeoffs of approach Y?
:::

Starter Block

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)

Hint Block

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 hint

Solution Block

The complete solution, hidden by default:

::: solution
```rust
fn solution() {
    // Complete implementation
}
```

### Explanation

Why this solution works...
:::

Tests Block

Test 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)

When mode=playground:

  • A "Run Tests" button appears
  • User code is combined with test code
  • Sent to play.rust-lang.org for execution
  • Results displayed inline

Reflection Block

Post-exercise questions:

::: reflection
- What did you learn from this exercise?
- How would you extend this solution?
:::

Browser Features

Test Execution

When tests have mode=playground, the preprocessor generates JavaScript that:

  1. Captures the user's code from the editable starter block
  2. Combines it with the test code
  3. Sends to the Rust Playground API
  4. Displays compilation errors or test results

Limitations:

  • Only works with std library (no external crates)
  • Subject to playground rate limits
  • Requires internet connection
  • ~5 second execution timeout

For exercises requiring external crates, use mode=local and guide users to run cargo test locally.

Progress Tracking

Exercise completion is tracked in localStorage:

  • Checkboxes next to learning objectives
  • "Mark Complete" button for exercises
  • Progress persists across sessions
  • No server required

Accessibility

  • All interactive elements are keyboard-accessible
  • Collapsible sections use proper ARIA attributes
  • High contrast mode supported
  • Screen reader announcements for test results

Configuration

book.toml options

[preprocessor.exercises]
# 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

Library Usage

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());

Feature Flags

[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" }

Integration with MCP Servers

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.

Examples

See the examples directory for complete exercise examples:

  • hello-world.md - Basic exercise structure
  • calculator.md - Multi-hint exercise with tests

Contributing

Contributions are welcome! Please open an issue or submit a pull request at GitHub.

License

MIT OR Apache-2.0