mdbook_core/
book.rs

1//! A tree structure representing a book.
2
3use serde::{Deserialize, Serialize};
4use std::collections::VecDeque;
5use std::fmt::{self, Display, Formatter};
6use std::ops::{Deref, DerefMut};
7use std::path::PathBuf;
8
9#[cfg(test)]
10mod tests;
11
12/// A tree structure representing a book.
13///
14/// A book is just a collection of [`BookItems`] which are accessible by
15/// either iterating (immutably) over the book with [`iter()`], or recursively
16/// applying a closure to each item to mutate the chapters, using
17/// [`for_each_mut()`].
18///
19/// [`iter()`]: #method.iter
20/// [`for_each_mut()`]: #method.for_each_mut
21#[allow(
22    clippy::exhaustive_structs,
23    reason = "This cannot be extended without breaking preprocessors."
24)]
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26pub struct Book {
27    /// The items in this book.
28    pub items: Vec<BookItem>,
29}
30
31impl Book {
32    /// Create an empty book.
33    pub fn new() -> Self {
34        Default::default()
35    }
36
37    /// Creates a new book with the given items.
38    pub fn new_with_items(items: Vec<BookItem>) -> Book {
39        Book { items }
40    }
41
42    /// Get a depth-first iterator over the items in the book.
43    pub fn iter(&self) -> BookItems<'_> {
44        BookItems {
45            items: self.items.iter().collect(),
46        }
47    }
48
49    /// A depth-first iterator over each [`Chapter`], skipping draft chapters.
50    pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
51        self.iter().filter_map(|item| match item {
52            BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch),
53            _ => None,
54        })
55    }
56
57    /// Recursively apply a closure to each item in the book, allowing you to
58    /// mutate them.
59    ///
60    /// # Note
61    ///
62    /// Unlike the `iter()` method, this requires a closure instead of returning
63    /// an iterator. This is because using iterators can possibly allow you
64    /// to have iterator invalidation errors.
65    pub fn for_each_mut<F>(&mut self, mut func: F)
66    where
67        F: FnMut(&mut BookItem),
68    {
69        for_each_mut(&mut func, &mut self.items);
70    }
71
72    /// Recursively apply a closure to each non-draft chapter in the book,
73    /// allowing you to mutate them.
74    pub fn for_each_chapter_mut<F>(&mut self, mut func: F)
75    where
76        F: FnMut(&mut Chapter),
77    {
78        for_each_mut(
79            &mut |item| {
80                let BookItem::Chapter(ch) = item else {
81                    return;
82                };
83                if ch.is_draft_chapter() {
84                    return;
85                }
86                func(ch)
87            },
88            &mut self.items,
89        );
90    }
91
92    /// Append a `BookItem` to the `Book`.
93    pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
94        self.items.push(item.into());
95        self
96    }
97}
98
99fn for_each_mut<'a, F, I>(func: &mut F, items: I)
100where
101    F: FnMut(&mut BookItem),
102    I: IntoIterator<Item = &'a mut BookItem>,
103{
104    for item in items {
105        if let BookItem::Chapter(ch) = item {
106            for_each_mut(func, &mut ch.sub_items);
107        }
108
109        func(item);
110    }
111}
112
113/// Enum representing any type of item which can be added to a book.
114#[allow(
115    clippy::exhaustive_enums,
116    reason = "This cannot be extended without breaking preprocessors."
117)]
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub enum BookItem {
120    /// A nested chapter.
121    Chapter(Chapter),
122    /// A section separator.
123    Separator,
124    /// A part title.
125    PartTitle(String),
126}
127
128impl From<Chapter> for BookItem {
129    fn from(other: Chapter) -> BookItem {
130        BookItem::Chapter(other)
131    }
132}
133
134/// The representation of a "chapter", usually mapping to a single file on
135/// disk however it may contain multiple sub-chapters.
136#[allow(
137    clippy::exhaustive_structs,
138    reason = "This cannot be extended without breaking preprocessors."
139)]
140#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
141pub struct Chapter {
142    /// The chapter's name.
143    pub name: String,
144    /// The chapter's contents.
145    pub content: String,
146    /// The chapter's section number, if it has one.
147    pub number: Option<SectionNumber>,
148    /// Nested items.
149    pub sub_items: Vec<BookItem>,
150    /// The chapter's location, relative to the `SUMMARY.md` file.
151    ///
152    /// **Note**: After the index preprocessor runs, any README files will be
153    /// modified to be `index.md`. If you need access to the actual filename
154    /// on disk, use [`Chapter::source_path`] instead.
155    ///
156    /// This is `None` for a draft chapter.
157    pub path: Option<PathBuf>,
158    /// The chapter's source file, relative to the `SUMMARY.md` file.
159    ///
160    /// **Note**: Beware that README files will internally be treated as
161    /// `index.md` via the [`Chapter::path`] field. The `source_path` field
162    /// exists if you need access to the true file path.
163    ///
164    /// This is `None` for a draft chapter, or a synthetically generated
165    /// chapter that has no file on disk.
166    pub source_path: Option<PathBuf>,
167    /// An ordered list of the names of each chapter above this one in the hierarchy.
168    pub parent_names: Vec<String>,
169}
170
171impl Chapter {
172    /// Create a new chapter with the provided content.
173    pub fn new<P: Into<PathBuf>>(
174        name: &str,
175        content: String,
176        p: P,
177        parent_names: Vec<String>,
178    ) -> Chapter {
179        let path: PathBuf = p.into();
180        Chapter {
181            name: name.to_string(),
182            content,
183            path: Some(path.clone()),
184            source_path: Some(path),
185            parent_names,
186            ..Default::default()
187        }
188    }
189
190    /// Create a new draft chapter that is not attached to a source markdown file (and thus
191    /// has no content).
192    pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
193        Chapter {
194            name: name.to_string(),
195            content: String::new(),
196            path: None,
197            source_path: None,
198            parent_names,
199            ..Default::default()
200        }
201    }
202
203    /// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
204    pub fn is_draft_chapter(&self) -> bool {
205        self.path.is_none()
206    }
207}
208
209impl Display for Chapter {
210    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
211        if let Some(ref section_number) = self.number {
212            write!(f, "{section_number} ")?;
213        }
214
215        write!(f, "{}", self.name)
216    }
217}
218
219/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
220/// a pretty `Display` impl.
221#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
222pub struct SectionNumber(Vec<u32>);
223
224impl SectionNumber {
225    /// Creates a new [`SectionNumber`].
226    pub fn new(numbers: impl Into<Vec<u32>>) -> SectionNumber {
227        SectionNumber(numbers.into())
228    }
229}
230
231impl Display for SectionNumber {
232    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
233        if self.0.is_empty() {
234            write!(f, "0")
235        } else {
236            for item in &self.0 {
237                write!(f, "{item}.")?;
238            }
239            Ok(())
240        }
241    }
242}
243
244impl Deref for SectionNumber {
245    type Target = Vec<u32>;
246    fn deref(&self) -> &Self::Target {
247        &self.0
248    }
249}
250
251impl DerefMut for SectionNumber {
252    fn deref_mut(&mut self) -> &mut Self::Target {
253        &mut self.0
254    }
255}
256
257impl FromIterator<u32> for SectionNumber {
258    fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
259        SectionNumber(it.into_iter().collect())
260    }
261}
262
263/// A depth-first iterator over the items in a book.
264///
265/// # Note
266///
267/// This struct shouldn't be created directly, instead prefer the
268/// [`Book::iter()`] method.
269pub struct BookItems<'a> {
270    items: VecDeque<&'a BookItem>,
271}
272
273impl<'a> Iterator for BookItems<'a> {
274    type Item = &'a BookItem;
275
276    fn next(&mut self) -> Option<Self::Item> {
277        let item = self.items.pop_front();
278
279        if let Some(BookItem::Chapter(ch)) = item {
280            // if we wanted a breadth-first iterator we'd `extend()` here
281            for sub_item in ch.sub_items.iter().rev() {
282                self.items.push_front(sub_item);
283            }
284        }
285
286        item
287    }
288}