mdbook-exercises 0.1.5

An mdBook preprocessor for interactive exercises with hints, solutions, and test execution
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# mdbook-exercises

A preprocessor for [mdBook](https://rust-lang.github.io/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

```bash
cargo install mdbook-exercises
```

### From source

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

### From GitHub directly

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

## Quick Start

### 1. Add to your book.toml

```toml
[preprocessor.exercises]
```

### 2. Include the CSS and JavaScript assets

Copy the assets to your book's theme directory:

```bash
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`:

```toml
[output.html]
additional-css = ["theme/exercises.css"]
additional-js = ["theme/exercises.js"]
```

### 3. Create an exercise in Markdown

````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

```bash
mdbook build
```

## Examples and Live Demo

- Browse ready-to-run examples in the `examples/` folder:
  - `hello-world.md` — Beginner Rust exercise with hints, solution, and Playground tests
  - `calculator.md` — Intermediate Rust with local tests
  - `multilang-python.md` — Python exercise (mode=local)
  - `multilang-js.md` — JavaScript exercise (mode=local)
  - `solution-reveal.md` — Demonstrates `reveal=always`
  - `ch02-environment-setup.md` — Setup exercise (ID suffix `-setup`, position `00`)
- Minimal sample mdBook (includes examples via `{{#exercise ...}}`): see `sample-book/`
- Live Demo: https://guyernest.github.io/mdbook-exercises/

## Directive Reference

### Exercise Block

Defines metadata for the exercise:

````markdown
::: 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:

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

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

### Discussion Block

Pre-exercise reflection prompts:

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

### Starter Block

Editable code for the student to complete:

````markdown
::: 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:
- You can also include the language and optional attributes in the fenced code block info string.
- Supported keys: `filename` or `file` for suggested filename.

Examples:

````markdown
::: starter
```rust,filename=src/lib.rs
pub fn run() {}
```
:::
````

````markdown
::: starter file="src/main.rs"
```rust
fn main() {}
```
:::
````

Precedence:
- If both the directive and the fence info specify the same property, the directive attribute wins. For example, `file="src/main.rs"` overrides `filename=...` in the fence.

### Hint Block

Progressive hints with levels:

````markdown
::: 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:

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

### Explanation

Why this solution works...
:::
````

Attributes:
- `reveal``on-demand` | `always` | `never` (controls visibility)

Rendering:
- The `reveal` attribute controls visibility:
  - `on-demand`: Hidden by default unless globally configured to reveal
  - `always`: Shown expanded regardless of global config
  - `never`: Kept hidden; the UI hides the toggle
- Hidden-by-default solutions have a "Show Solution" control

### Tests Block

Test code that can optionally run in the browser:

````markdown
::: 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`:
- A "Run Tests" button appears
- User code is combined with test code
- Sent to play.rust-lang.org for execution
- Results displayed inline

Code fence info:
- The fence language (e.g., ```` ```rust ````) sets `language` if the directive doesn’t specify it.
- If both are present, the directive attribute `language=...` takes precedence over the fence.

Implementation details:
- Playground execution runs tests as a library (crateType `lib`), combining starter code with test code.

### Reflection Block

Post-exercise questions:

````markdown
::: 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

```toml
[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
```

## Library Usage

`mdbook-exercises` can be used as a library for parsing exercise markdown:

```rust
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

```toml
[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:

```rust
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](./DESIGN.md) for details on MCP integration patterns.

## Examples

See the [examples](./examples) directory for complete exercise examples:

- `hello-world.md` - Basic exercise structure
- `calculator.md` - Multi-hint exercise with tests
- `multilang-python.md` - Non-Rust example (Python), local tests
- `double-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](https://guyernest.github.io/mdbook-exercises/)

### Course Developer Guides

- Organizing and integrating exercises (include-only pattern): see `docs/exercises-integration.md`.
- Setup exercises (ID suffix `-setup`, position `00`): see `docs/setup-exercises.md`.

### Sample mdBook

See the [sample-book](./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.md`
- `src/exercises.md` demonstrates including exercises via `{{#exercise ...}}` from this repository’s examples.

### Using with mdbook-quiz

You can use mdbook-exercises alongside mdbook-quiz. A common book.toml setup looks like:

```toml
[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)
```

## Precedence Rules

- Starter:
  - Directive attributes override fence info (e.g., `file="..."` beats `filename=...`).
  - Fence language sets default when `language` attribute is omitted.
- Tests:
  - Directive `language=...` overrides fence language. Fence language sets default when omitted.
  - `mode` is taken from directive attributes.
- Solution:
  - `reveal` on the solution overrides global config (`reveal_solution`).

## Contributing

Contributions are welcome! Please open an issue or submit a pull request at [GitHub](https://github.com/guyernest/mdbook-exercises).

## License

MIT OR Apache-2.0