1#![deny(
4 clippy::all,
5 missing_debug_implementations,
6 missing_copy_implementations,
7 missing_docs
8)]
9#![warn(clippy::pedantic, clippy::nursery)]
10
11use mdbook_preprocessor::{
12 book::{Book, BookItem, Chapter},
13 errors::Error,
14 Preprocessor, PreprocessorContext,
15};
16use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
17use pulldown_cmark_to_cmark::{calculate_code_block_token_count, cmark_with_options};
18
19mod backend;
20use backend::{Backend, RenderContext};
21
22mod config;
23
24#[derive(Default, Clone, Copy, Debug)]
26pub struct D2;
27
28impl Preprocessor for D2 {
29 fn name(&self) -> &'static str {
30 "d2"
31 }
32
33 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
34 let backend = Backend::from_context(ctx);
35
36 book.for_each_mut(|section| {
37 if let BookItem::Chapter(chapter) = section {
38 let events: Vec<_> = process_events(
39 &backend,
40 chapter,
41 Parser::new_ext(&chapter.content, Options::all()),
42 )
43 .collect();
44
45 let code_block_token_count =
51 calculate_code_block_token_count(events.iter()).unwrap_or(3);
52
53 let options = pulldown_cmark_to_cmark::Options {
54 code_block_token_count,
55 ..Default::default()
56 };
57
58 let mut buf = String::with_capacity(chapter.content.len() + 128);
60
61 cmark_with_options(events.into_iter(), &mut buf, options).unwrap();
63 chapter.content = buf;
64 }
65 });
66
67 Ok(book)
68 }
69}
70
71fn process_events<'a>(
72 backend: &'a Backend,
73 chapter: &'a Chapter,
74 events: impl Iterator<Item = Event<'a>> + 'a,
75) -> impl Iterator<Item = Event<'a>> + 'a {
76 let mut in_block = false;
77 let mut diagram = String::new();
81 let mut diagram_index = 0;
82
83 events.flat_map(move |event| {
84 match (&event, in_block) {
85 (
87 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed("d2")))),
88 false,
89 ) => {
90 in_block = true;
91 diagram.clear();
92 diagram_index += 1;
93 vec![]
94 }
95 (Event::Text(content), true) => {
97 diagram.push_str(content);
98 vec![]
99 }
100 (Event::End(TagEnd::CodeBlock), true) => {
102 in_block = false;
103 let render_context = RenderContext::new(
104 chapter.source_path.as_ref().unwrap(),
105 &chapter.name,
106 chapter.number.as_ref(),
107 diagram_index,
108 );
109 backend
110 .render(&render_context, &diagram)
111 .unwrap_or_else(|e| {
112 eprintln!("{e}");
115 vec![]
116 })
117 }
118 _ => vec![event],
120 }
121 })
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn round_trip_markdown(input: &str) -> String {
130 let events: Vec<_> = Parser::new_ext(input, Options::all()).collect();
131 let code_block_token_count = calculate_code_block_token_count(events.iter()).unwrap_or(3);
132 let options = pulldown_cmark_to_cmark::Options {
133 code_block_token_count,
134 ..Default::default()
135 };
136 let mut output = String::new();
137 cmark_with_options(events.into_iter(), &mut output, options).unwrap();
138 output
139 }
140
141 #[test]
148 fn code_blocks_preserve_backticks() {
149 let input = "```rust\nfn main() {}\n```\n";
150 let output = round_trip_markdown(input);
151
152 assert!(
153 output.contains("```rust"),
154 "expected 3 backticks, got: {output}"
155 );
156 assert!(
157 !output.contains("````"),
158 "should not have 4 backticks: {output}"
159 );
160 }
161
162 #[test]
163 fn multiple_code_blocks_preserve_backticks() {
164 let input = r#"# Title
165
166```rust
167fn main() {}
168```
169
170Some text.
171
172```python
173print("hello")
174```
175"#;
176
177 let output = round_trip_markdown(input);
178
179 let three_backticks = output.matches("```").count();
181 let four_backticks = output.matches("````").count();
182
183 assert_eq!(
184 three_backticks, 4,
185 "expected 4 occurrences of 3 backticks (2 code blocks × 2), got: {output}"
186 );
187 assert_eq!(
188 four_backticks, 0,
189 "should not have any 4 backticks: {output}"
190 );
191 }
192
193 #[test]
196 fn nested_code_blocks_escaped_correctly() {
197 let input = r"Here's how to write a code block:
199
200````markdown
201```rust
202fn main() {}
203```
204````
205
206That's it!
207";
208
209 let output = round_trip_markdown(input);
210
211 assert!(
213 output.contains("````"),
214 "should have 4 backticks to escape nested block: {output}"
215 );
216 assert!(
218 output.contains("```rust"),
219 "inner code block should be preserved: {output}"
220 );
221 }
222}