mdbook_iced/
lib.rs

1mod compiler;
2
3use compiler::Compiler;
4
5use anyhow::Error;
6use mdbook::book::{Book, BookItem, Chapter};
7use mdbook::preprocess::PreprocessorContext;
8use semver::{Version, VersionReq};
9
10use std::collections::BTreeSet;
11use std::fs;
12use std::path::Path;
13
14pub fn is_supported(renderer: &str) -> bool {
15    renderer == "html"
16}
17
18pub fn run(mut book: Book, context: &PreprocessorContext) -> Result<Book, Error> {
19    let book_version = Version::parse(&context.mdbook_version)?;
20    let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
21
22    if !version_req.matches(&book_version) {
23        return Err(Error::msg(format!(
24            "mdbook-iced plugin version ({}) is not compatible \
25            with the book version ({})",
26            mdbook::MDBOOK_VERSION,
27            context.mdbook_version
28        )));
29    }
30
31    let config = context
32        .config
33        .get_preprocessor("iced")
34        .ok_or(Error::msg("mdbook-iced configuration not found"))?;
35
36    let reference = compiler::Reference::parse(config)?;
37    let compiler = Compiler::set_up(&context.root, reference)?;
38
39    let mut icebergs = BTreeSet::new();
40
41    for section in &mut book.sections {
42        if let BookItem::Chapter(chapter) = section {
43            let (content, new_icebergs) = process_chapter(&compiler, chapter)?;
44
45            chapter.content = content;
46            icebergs.extend(new_icebergs);
47        }
48    }
49
50    let target = context.root.join("src").join(".icebergs");
51    fs::create_dir_all(&target)?;
52
53    compiler.retain(&icebergs)?;
54    compiler.release(&icebergs, target)?;
55
56    Ok(book)
57}
58
59fn process_chapter(
60    compiler: &Compiler,
61    chapter: &Chapter,
62) -> Result<(String, BTreeSet<compiler::Iceberg>), Error> {
63    use itertools::Itertools;
64    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
65    use pulldown_cmark_to_cmark::cmark;
66
67    let events = Parser::new_ext(&chapter.content, Options::all());
68
69    let mut in_iced_code = false;
70
71    let groups = events.group_by(|event| match event {
72        Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(label)))
73            if label.starts_with("rust")
74                && label
75                    .split(',')
76                    .any(|modifier| modifier.starts_with("iced")) =>
77        {
78            in_iced_code = true;
79            true
80        }
81        Event::End(TagEnd::CodeBlock) => {
82            let is_iced_code = in_iced_code;
83
84            in_iced_code = false;
85
86            is_iced_code
87        }
88        _ => in_iced_code,
89    });
90
91    let mut icebergs = Vec::new();
92    let mut heights = Vec::new();
93    let mut is_first = true;
94
95    let output = groups.into_iter().flat_map(|(is_iced_code, group)| {
96        if is_iced_code {
97            let mut events = Vec::new();
98            let mut code = String::new();
99
100            for event in group {
101                if let Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(label))) = &event {
102                    let height = label
103                        .split(',')
104                        .find(|modifier| modifier.starts_with("iced"))
105                        .and_then(|modifier| {
106                            Some(
107                                modifier
108                                    .strip_prefix("iced(")?
109                                    .strip_suffix(')')?
110                                    .split_once("height=")?
111                                    .1
112                                    .to_string(),
113                            )
114                        });
115
116                    code.clear();
117                    icebergs.push(None);
118                    heights.push(height);
119                    events.push(event);
120                } else if let Event::Text(text) = &event {
121                    if !code.ends_with('\n') {
122                        code.push('\n');
123                    }
124
125                    code.push_str(text);
126                    events.push(event);
127                } else if let Event::End(TagEnd::CodeBlock) = &event {
128                    events.push(event);
129
130                    if let Ok(iceberg) = compiler.compile(&code) {
131                        if let Some(last_iceberg) = icebergs.last_mut() {
132                            *last_iceberg = Some(iceberg);
133                        }
134                    }
135
136                    if is_first {
137                        is_first = false;
138
139                        events.push(Event::InlineHtml(compiler::Iceberg::LIBRARY.into()));
140                    }
141
142                    if let Some(iceberg) = icebergs.last().and_then(Option::as_ref) {
143                        events.push(Event::InlineHtml(
144                            iceberg
145                                .embed(heights.last().and_then(Option::as_deref))
146                                .into(),
147                        ));
148                    }
149                } else {
150                    events.push(event);
151                }
152            }
153
154            Box::new(events.into_iter())
155        } else {
156            Box::new(group) as Box<dyn Iterator<Item = Event>>
157        }
158    });
159
160    let mut content = String::with_capacity(chapter.content.len());
161    let _ = cmark(output, &mut content)?;
162
163    Ok((content, icebergs.into_iter().flatten().collect()))
164}
165
166pub fn clean(root: impl AsRef<Path>) -> Result<(), Error> {
167    let book_toml = root.as_ref().join("book.toml");
168    if !book_toml.exists() {
169        return Err(Error::msg(
170            "book.toml not found in the current directory. This command \
171            can only be run in an mdBook project.",
172        ));
173    }
174
175    let output = root.as_ref().join("src").join(".icebergs");
176    fs::remove_dir_all(output)?;
177
178    Compiler::clean(root)?;
179
180    Ok(())
181}