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}