mdbook_d2/
lib.rs

1//! [D2] diagram generator [`Preprocessor`] library for [`MdBook`](https://rust-lang.github.io/mdBook/).
2
3#![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::cmark;
18
19mod backend;
20use backend::{Backend, RenderContext};
21
22mod config;
23
24/// [D2] diagram generator [`Preprocessor`] for [`MdBook`](https://rust-lang.github.io/mdBook/).
25#[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 = process_events(
39                    &backend,
40                    chapter,
41                    Parser::new_ext(&chapter.content, Options::all()),
42                );
43
44                // create a buffer in which we can place the markdown
45                let mut buf = String::with_capacity(chapter.content.len() + 128);
46
47                // convert it back to markdown and replace the original chapter's content
48                cmark(events, &mut buf).unwrap();
49                chapter.content = buf;
50            }
51        });
52
53        Ok(book)
54    }
55}
56
57fn process_events<'a>(
58    backend: &'a Backend,
59    chapter: &'a Chapter,
60    events: impl Iterator<Item = Event<'a>> + 'a,
61) -> impl Iterator<Item = Event<'a>> + 'a {
62    let mut in_block = false;
63    // if Windows crlf line endings are used, a code block will consist
64    // of many different Text blocks, thus we need to buffer them in here
65    // see https://github.com/raphlinus/pulldown-cmark/issues/507
66    let mut diagram = String::new();
67    let mut diagram_index = 0;
68
69    events.flat_map(move |event| {
70        match (&event, in_block) {
71            // check if we are entering a d2 codeblock
72            (
73                Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed("d2")))),
74                false,
75            ) => {
76                in_block = true;
77                diagram.clear();
78                diagram_index += 1;
79                vec![]
80            }
81            // check if we are currently inside a d2 block
82            (Event::Text(content), true) => {
83                diagram.push_str(content);
84                vec![]
85            }
86            // check if we are exiting a d2 block
87            (Event::End(TagEnd::CodeBlock), true) => {
88                in_block = false;
89                let render_context = RenderContext::new(
90                    chapter.source_path.as_ref().unwrap(),
91                    &chapter.name,
92                    chapter.number.as_ref(),
93                    diagram_index,
94                );
95                backend
96                    .render(&render_context, &diagram)
97                    .unwrap_or_else(|e| {
98                        // if we cannot render the diagram, print the error and return an empty
99                        // block
100                        eprintln!("{e}");
101                        vec![]
102                    })
103            }
104            // if nothing matches, change nothing
105            _ => vec![event],
106        }
107    })
108}