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